Skip to content

Commit 63aea7a

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

File tree

5 files changed

+309
-0
lines changed

5 files changed

+309
-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: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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 title={this._trans.__('Current disk usage')} source={text} />
59+
);
60+
} else {
61+
return (
62+
<TextItem
63+
title={this._trans.__('Current disk usage')}
64+
source={text}
65+
className={resourceItem}
66+
/>
67+
);
68+
}
69+
}
70+
71+
private _trans: TranslationBundle;
72+
}
73+
74+
/**
75+
* A namespace for DiskUsage statics.
76+
*/
77+
export namespace DiskUsage {
78+
/**
79+
* A VDomModel for the disk usage status item.
80+
*/
81+
export class Model extends VDomModel {
82+
/**
83+
* Construct a new disk usage model.
84+
*
85+
* @param options: the options for creating the model.
86+
*/
87+
constructor(options: Model.IOptions) {
88+
super();
89+
this._poll = new Poll<Private.IMetricRequestResult>({
90+
factory: () => Private.factory(),
91+
frequency: {
92+
interval: options.refreshRate,
93+
backoff: true,
94+
},
95+
name: '@jupyterlab/statusbar:DiskUsage#metrics',
96+
});
97+
this._poll.ticked.connect((poll) => {
98+
const { payload, phase } = poll.state;
99+
if (phase === 'resolved') {
100+
this._updateMetricsValues(payload);
101+
return;
102+
}
103+
if (phase === 'rejected') {
104+
const oldMetricsAvailable = this._metricsAvailable;
105+
this._metricsAvailable = false;
106+
this._diskUsed = 0;
107+
this._diskTotal = null;
108+
this._units = 'B';
109+
110+
if (oldMetricsAvailable) {
111+
this.stateChanged.emit();
112+
}
113+
return;
114+
}
115+
});
116+
}
117+
118+
/**
119+
* Whether the metrics server extension is available.
120+
*/
121+
get metricsAvailable(): boolean {
122+
return this._metricsAvailable;
123+
}
124+
125+
/**
126+
* The current disk usage
127+
*/
128+
get diskUsed(): number {
129+
return this._diskUsed;
130+
}
131+
132+
/**
133+
* The current disk limit, or null if not specified.
134+
*/
135+
get diskTotal(): number | null {
136+
return this._diskTotal;
137+
}
138+
139+
/**
140+
* The units for disk usages and limits.
141+
*/
142+
get units(): MemoryUnit {
143+
return this._units;
144+
}
145+
146+
/**
147+
* The warning for disk usage.
148+
*/
149+
get usageWarning(): boolean {
150+
return this._warn;
151+
}
152+
153+
/**
154+
* Dispose of the memory usage model.
155+
*/
156+
dispose(): void {
157+
super.dispose();
158+
this._poll.dispose();
159+
}
160+
161+
/**
162+
* Given the results of the metrics request, update model values.
163+
*/
164+
private _updateMetricsValues(
165+
value: Private.IMetricRequestResult | null
166+
): void {
167+
const oldMetricsAvailable = this._metricsAvailable;
168+
const oldDiskUsed = this._diskUsed;
169+
const oldDiskTotal = this._diskTotal;
170+
const oldUnits = this._units;
171+
172+
if (value === null) {
173+
this._metricsAvailable = false;
174+
this._diskUsed = 0;
175+
this._diskTotal = null;
176+
this._units = 'B';
177+
this._warn = false;
178+
} else {
179+
const diskUsedBytes = value.disk_used;
180+
const diskTotal = value.disk_total;
181+
const [diskUsed, units] = convertToLargestUnit(diskUsedBytes);
182+
183+
this._metricsAvailable = true;
184+
this._diskUsed = diskUsed;
185+
this._units = units;
186+
this._diskTotal = diskTotal
187+
? diskTotal / MEMORY_UNIT_LIMITS[units]
188+
: null;
189+
}
190+
191+
if (
192+
this._diskUsed !== oldDiskUsed ||
193+
this._units !== oldUnits ||
194+
this._diskTotal !== oldDiskTotal ||
195+
this._metricsAvailable !== oldMetricsAvailable
196+
) {
197+
this.stateChanged.emit(void 0);
198+
}
199+
}
200+
201+
private _diskUsed = 0;
202+
private _diskTotal: number | null = null;
203+
private _metricsAvailable = false;
204+
private _poll: Poll<Private.IMetricRequestResult>;
205+
private _units: MemoryUnit = 'B';
206+
private _warn = false;
207+
}
208+
209+
/**
210+
* A namespace for Model statics.
211+
*/
212+
export namespace Model {
213+
/**
214+
* Options for creating a DiskUsage model.
215+
*/
216+
export interface IOptions {
217+
/**
218+
* The refresh rate (in ms) for querying the server.
219+
*/
220+
refreshRate: number;
221+
}
222+
}
223+
}
224+
225+
/**
226+
* A namespace for module private statics.
227+
*/
228+
namespace Private {
229+
/**
230+
* The number of decimal places to use when rendering disk usage.
231+
*/
232+
export const DECIMAL_PLACES = 2;
233+
234+
/**
235+
* Settings for making requests to the server.
236+
*/
237+
const SERVER_CONNECTION_SETTINGS = ServerConnection.makeSettings();
238+
239+
/**
240+
* The url endpoint for making requests to the server.
241+
*/
242+
const METRIC_URL = URLExt.join(
243+
SERVER_CONNECTION_SETTINGS.baseUrl,
244+
'api/metrics/v1'
245+
);
246+
247+
/**
248+
* The shape of a response from the metrics server extension.
249+
*/
250+
export interface IMetricRequestResult {
251+
disk_used: number;
252+
disk_total: number;
253+
}
254+
255+
/**
256+
* Make a request to the backend.
257+
*/
258+
export async function factory(): Promise<IMetricRequestResult> {
259+
const request = ServerConnection.makeRequest(
260+
METRIC_URL,
261+
{},
262+
SERVER_CONNECTION_SETTINGS
263+
);
264+
const response = await request;
265+
266+
return await response.json();
267+
}
268+
}

packages/labextension/src/index.ts

Lines changed: 25 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,28 @@ 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+
4973
const kernelUsagePlugin: JupyterFrontEndPlugin<void> = {
5074
id: '@jupyter-server/resource-usage:kernel-panel-item',
5175
autoStart: true,
@@ -102,6 +126,7 @@ const kernelUsagePlugin: JupyterFrontEndPlugin<void> = {
102126

103127
const plugins: JupyterFrontEndPlugin<any>[] = [
104128
memoryStatusPlugin,
129+
diskStatusPlugin,
105130
kernelUsagePlugin,
106131
];
107132
export default plugins;

0 commit comments

Comments
 (0)