Skip to content

Commit 201d3b1

Browse files
committed
Track disk utilization in $HOME
Similar to memory usage tracking, polls the disk used/available in $HOME.
1 parent a2f5579 commit 201d3b1

File tree

5 files changed

+314
-0
lines changed

5 files changed

+314
-0
lines changed

jupyter_resource_usage/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from concurrent.futures import ThreadPoolExecutor
33
from inspect import isawaitable
44

5+
import os
56
import psutil
67
import zmq.asyncio
78
from jupyter_client.jsonutil import date_default
@@ -58,6 +59,11 @@ async def get(self):
5859
)
5960

6061
metrics = {"rss": rss, "limits": limits}
62+
63+
disk_info = psutil.disk_usage(os.getenv("HOME"))
64+
metrics["disk_used"] = disk_info.used
65+
metrics["disk_total"] = disk_info.total
66+
6167
if pss is not None:
6268
metrics["pss"] = pss
6369

jupyter_resource_usage/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ class ResourceUseDisplay(Configurable):
3737
Holds server-side configuration for jupyter-resource-usage
3838
"""
3939

40+
system_disk_metrics = List(
41+
trait=PSUtilMetric(),
42+
default_value=[{"name": "disk_usage", "attribute": "disk_used"}],
43+
)
44+
4045
process_memory_metrics = List(
4146
trait=PSUtilMetric(),
4247
default_value=[{"name": "memory_info", "attribute": "rss"}],

jupyter_resource_usage/metrics.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,8 @@ def cpu_metrics(self):
8080
return self.metrics(
8181
self.config.process_cpu_metrics, self.config.system_cpu_metrics
8282
)
83+
84+
def disk_metrics(self):
85+
return self.metrics(
86+
self.config.system_disk_metrics
87+
)
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import { VDomModel, VDomRenderer } from '@jupyterlab/apputils';
5+
6+
import { URLExt } from '@jupyterlab/coreutils';
7+
8+
import { TextItem } from '@jupyterlab/statusbar';
9+
10+
import { ServerConnection } from '@jupyterlab/services';
11+
12+
import { TranslationBundle } from '@jupyterlab/translation';
13+
14+
import { Poll } from '@lumino/polling';
15+
16+
import React from 'react';
17+
18+
import { MemoryUnit, MEMORY_UNIT_LIMITS, convertToLargestUnit } from './format';
19+
20+
import { resourceItem } from './text';
21+
22+
/**
23+
* A VDomRenderer for showing disk usage by a kernel.
24+
*/
25+
export class DiskUsage extends VDomRenderer<DiskUsage.Model> {
26+
/**
27+
* Construct a new disk usage status item.
28+
*/
29+
constructor(trans: TranslationBundle) {
30+
super(new DiskUsage.Model({ refreshRate: 5000 }));
31+
this._trans = trans;
32+
}
33+
34+
/**
35+
* Render the disk usage status item.
36+
*/
37+
render(): JSX.Element {
38+
if (!this.model) {
39+
return <div></div>;
40+
}
41+
let text: string;
42+
if (this.model.diskTotal === null) {
43+
text = this._trans.__(
44+
'Disk: %1 %2',
45+
this.model.diskUsed.toFixed(Private.DECIMAL_PLACES),
46+
this.model.units
47+
);
48+
} else {
49+
text = this._trans.__(
50+
'Disk Usage: %1 / %2 %3',
51+
this.model.diskUsed.toFixed(Private.DECIMAL_PLACES),
52+
this.model.diskTotal.toFixed(Private.DECIMAL_PLACES),
53+
this.model.units
54+
);
55+
}
56+
if (!this.model.usageWarning) {
57+
return (
58+
<TextItem
59+
title={this._trans.__('Current disk usage')}
60+
source={text}
61+
/>
62+
);
63+
} else {
64+
return (
65+
<TextItem
66+
title={this._trans.__('Current disk usage')}
67+
source={text}
68+
className={resourceItem}
69+
/>
70+
);
71+
}
72+
}
73+
74+
private _trans: TranslationBundle;
75+
}
76+
77+
/**
78+
* A namespace for DiskUsage statics.
79+
*/
80+
export namespace DiskUsage {
81+
/**
82+
* A VDomModel for the disk usage status item.
83+
*/
84+
export class Model extends VDomModel {
85+
/**
86+
* Construct a new disk usage model.
87+
*
88+
* @param options: the options for creating the model.
89+
*/
90+
constructor(options: Model.IOptions) {
91+
super();
92+
this._poll = new Poll<Private.IMetricRequestResult>({
93+
factory: () => Private.factory(),
94+
frequency: {
95+
interval: options.refreshRate,
96+
backoff: true,
97+
},
98+
name: '@jupyterlab/statusbar:DiskUsage#metrics',
99+
});
100+
this._poll.ticked.connect((poll) => {
101+
const { payload, phase } = poll.state;
102+
if (phase === 'resolved') {
103+
this._updateMetricsValues(payload);
104+
return;
105+
}
106+
if (phase === 'rejected') {
107+
const oldMetricsAvailable = this._metricsAvailable;
108+
this._metricsAvailable = false;
109+
this._diskUsed = 0;
110+
this._diskTotal = null;
111+
this._units = 'B';
112+
113+
if (oldMetricsAvailable) {
114+
this.stateChanged.emit();
115+
}
116+
return;
117+
}
118+
});
119+
}
120+
121+
/**
122+
* Whether the metrics server extension is available.
123+
*/
124+
get metricsAvailable(): boolean {
125+
return this._metricsAvailable;
126+
}
127+
128+
/**
129+
* The current disk usage
130+
*/
131+
get diskUsed(): number {
132+
return this._diskUsed;
133+
}
134+
135+
/**
136+
* The current disk limit, or null if not specified.
137+
*/
138+
get diskTotal(): number | null {
139+
return this._diskTotal;
140+
}
141+
142+
/**
143+
* The units for disk usages and limits.
144+
*/
145+
get units(): MemoryUnit {
146+
return this._units;
147+
}
148+
149+
/**
150+
* The warning for disk usage.
151+
*/
152+
get usageWarning(): boolean {
153+
return this._warn;
154+
}
155+
156+
/**
157+
* Dispose of the memory usage model.
158+
*/
159+
dispose(): void {
160+
super.dispose();
161+
this._poll.dispose();
162+
}
163+
164+
/**
165+
* Given the results of the metrics request, update model values.
166+
*/
167+
private _updateMetricsValues(
168+
value: Private.IMetricRequestResult | null
169+
): void {
170+
const oldMetricsAvailable = this._metricsAvailable;
171+
const oldDiskUsed = this._diskUsed;
172+
const oldDiskTotal = this._diskTotal;
173+
const oldUnits = this._units;
174+
175+
if (value === null) {
176+
this._metricsAvailable = false;
177+
this._diskUsed = 0;
178+
this._diskTotal = null;
179+
this._units = 'B';
180+
this._warn = false;
181+
} else {
182+
const diskUsedBytes = value.disk_used;
183+
const diskTotal = value.disk_total;
184+
const [diskUsed, units] = convertToLargestUnit(diskUsedBytes);
185+
186+
this._metricsAvailable = true;
187+
this._diskUsed = diskUsed;
188+
this._units = units;
189+
this._diskTotal = diskTotal
190+
? diskTotal / MEMORY_UNIT_LIMITS[units]
191+
: null;
192+
}
193+
194+
if (
195+
this._diskUsed !== oldDiskUsed ||
196+
this._units !== oldUnits ||
197+
this._diskTotal !== oldDiskTotal ||
198+
this._metricsAvailable !== oldMetricsAvailable
199+
) {
200+
this.stateChanged.emit(void 0);
201+
}
202+
}
203+
204+
private _diskUsed = 0;
205+
private _diskTotal: number | null = null;
206+
private _metricsAvailable = false;
207+
private _poll: Poll<Private.IMetricRequestResult>;
208+
private _units: MemoryUnit = 'B';
209+
private _warn = false;
210+
}
211+
212+
/**
213+
* A namespace for Model statics.
214+
*/
215+
export namespace Model {
216+
/**
217+
* Options for creating a DiskUsage model.
218+
*/
219+
export interface IOptions {
220+
/**
221+
* The refresh rate (in ms) for querying the server.
222+
*/
223+
refreshRate: number;
224+
}
225+
}
226+
}
227+
228+
/**
229+
* A namespace for module private statics.
230+
*/
231+
namespace Private {
232+
/**
233+
* The number of decimal places to use when rendering disk usage.
234+
*/
235+
export const DECIMAL_PLACES = 2;
236+
237+
/**
238+
* Settings for making requests to the server.
239+
*/
240+
const SERVER_CONNECTION_SETTINGS = ServerConnection.makeSettings();
241+
242+
/**
243+
* The url endpoint for making requests to the server.
244+
*/
245+
const METRIC_URL = URLExt.join(
246+
SERVER_CONNECTION_SETTINGS.baseUrl,
247+
'api/metrics/v1'
248+
);
249+
250+
/**
251+
* The shape of a response from the metrics server extension.
252+
*/
253+
export interface IMetricRequestResult {
254+
disk_used: number;
255+
disk_total: number;
256+
}
257+
258+
/**
259+
* Make a request to the backend.
260+
*/
261+
export async function factory(): Promise<IMetricRequestResult> {
262+
const request = ServerConnection.makeRequest(
263+
METRIC_URL,
264+
{},
265+
SERVER_CONNECTION_SETTINGS
266+
);
267+
const response = await request;
268+
269+
return await response.json();
270+
}
271+
}

packages/labextension/src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { IStatusBar } from '@jupyterlab/statusbar';
1515
import { ITranslator } from '@jupyterlab/translation';
1616

1717
import { MemoryUsage } from './memoryUsage';
18+
import { DiskUsage } from './diskUsage';
19+
1820
import { KernelWidgetTracker } from './tracker';
1921

2022
namespace CommandIDs {
@@ -46,6 +48,30 @@ const memoryStatusPlugin: JupyterFrontEndPlugin<void> = {
4648
},
4749
};
4850

51+
const diskStatusPlugin: JupyterFrontEndPlugin<void> = {
52+
id: '@jupyter-server/resource-usage:disk-status-item',
53+
autoStart: true,
54+
requires: [IStatusBar, ITranslator],
55+
activate: (
56+
app: JupyterFrontEnd,
57+
statusBar: IStatusBar,
58+
translator: ITranslator
59+
) => {
60+
const trans = translator.load('jupyter-resource-usage');
61+
const item = new DiskUsage(trans);
62+
63+
statusBar.registerStatusItem(diskStatusPlugin.id, {
64+
item,
65+
align: 'left',
66+
rank: 2,
67+
isActive: () => item.model.metricsAvailable,
68+
activeStateChanged: item.model.stateChanged,
69+
});
70+
},
71+
};
72+
73+
74+
4975
const kernelUsagePlugin: JupyterFrontEndPlugin<void> = {
5076
id: '@jupyter-server/resource-usage:kernel-panel-item',
5177
autoStart: true,
@@ -102,6 +128,7 @@ const kernelUsagePlugin: JupyterFrontEndPlugin<void> = {
102128

103129
const plugins: JupyterFrontEndPlugin<any>[] = [
104130
memoryStatusPlugin,
131+
diskStatusPlugin,
105132
kernelUsagePlugin,
106133
];
107134
export default plugins;

0 commit comments

Comments
 (0)