diff --git a/package.json b/package.json index 6c37e09..9a59afd 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@jupyterlab/coreutils": "^6.0.0", "@jupyterlab/launcher": "^4.0.5", "@jupyterlab/services": "^7.0.0", + "@jupyterlab/settingregistry": "^4.1.0", "d3-format": "^3.1.0", "d3-scale": "^4.0.2", "react": "^18.2.0", diff --git a/schema/config.json b/schema/config.json new file mode 100644 index 0000000..1f4f84a --- /dev/null +++ b/schema/config.json @@ -0,0 +1,15 @@ +{ + "title": "jupyterlab-nvdashboard", + "description": "Settings for the jupyterlab-nvdashboard extension.", + "type": "object", + "properties": { + "updateFrequency": { + "type": "integer", + "title": "Frequency of Updates", + "description": "The frequency of updates for the GPU Dashboard widgets, in milliseconds.", + "default": 100, + "minimum": 1 + } + }, + "additionalProperties": false +} diff --git a/src/assets/constants.ts b/src/assets/constants.ts index fdffb0b..743be32 100644 --- a/src/assets/constants.ts +++ b/src/assets/constants.ts @@ -1,2 +1,9 @@ export const BAR_COLOR_LINEAR_RANGE: string[] = ['#ff7900', '#b30000']; export const GPU_COLOR_CATEGORICAL_RANGE: string[] = ['#fecc5c', '#bd0026']; +export const PLUGIN_ID = 'jupyterlab-nvdashboard'; +export const PLUGIN_ID_CONFIG = `${PLUGIN_ID}:config`; +export const PLUGIN_ID_OPEN_SETTINGS = `${PLUGIN_ID}:open-settings`; +export const WIDGET_TRACKER_NAME = 'gpu-dashboard-widgets'; +export const COMMAND_OPEN_SETTINGS = 'settingeditor:open'; +export const COMMAND_OPEN_WIDGET = 'gpu-dashboard-widget:open'; +export const DEFAULT_UPDATE_FREQUENCY = 100; // ms diff --git a/src/assets/hooks.ts b/src/assets/hooks.ts new file mode 100644 index 0000000..a9ee9bc --- /dev/null +++ b/src/assets/hooks.ts @@ -0,0 +1,35 @@ +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { SetStateAction, useEffect } from 'react'; +import { DEFAULT_UPDATE_FREQUENCY, PLUGIN_ID_CONFIG } from './constants'; + +function loadSettingRegistry( + settingRegistry: ISettingRegistry, + setUpdateFrequency: { + (value: SetStateAction): void; + (arg0: number): void; + } +) { + useEffect(() => { + const loadSettings = async () => { + try { + const settings = await settingRegistry.load(PLUGIN_ID_CONFIG); + const loadedUpdateFrequency = + (settings.get('updateFrequency').composite as number) || + DEFAULT_UPDATE_FREQUENCY; + setUpdateFrequency(loadedUpdateFrequency); + + settings.changed.connect(() => { + setUpdateFrequency( + (settings.get('updateFrequency').composite as number) || + DEFAULT_UPDATE_FREQUENCY + ); + }); + } catch (error) { + console.error(`An error occurred while loading settings: ${error}`); + } + }; + loadSettings(); + }, []); +} + +export default loadSettingRegistry; diff --git a/src/assets/interfaces.ts b/src/assets/interfaces.ts new file mode 100644 index 0000000..7f8118b --- /dev/null +++ b/src/assets/interfaces.ts @@ -0,0 +1,20 @@ +import { ILabShell, JupyterFrontEnd } from '@jupyterlab/application'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { MainAreaWidget, WidgetTracker } from '@jupyterlab/apputils'; + +export interface IChartProps { + settingRegistry: ISettingRegistry; +} + +export interface IControlProps { + app: JupyterFrontEnd; + labShell: ILabShell; + tracker: WidgetTracker; + settingRegistry: ISettingRegistry; +} + +export interface IWidgetInfo { + id: string; + title: string; + instance: MainAreaWidget; +} diff --git a/src/charts/GpuMemoryChart.tsx b/src/charts/GpuMemoryChart.tsx index bca1eea..baed803 100644 --- a/src/charts/GpuMemoryChart.tsx +++ b/src/charts/GpuMemoryChart.tsx @@ -12,13 +12,26 @@ import { } from 'recharts'; import { scaleLinear } from 'd3-scale'; import { renderCustomTooltip } from '../components/tooltipUtils'; -import { BAR_COLOR_LINEAR_RANGE } from '../assets/constants'; +import { + BAR_COLOR_LINEAR_RANGE, + DEFAULT_UPDATE_FREQUENCY +} from '../assets/constants'; import { format } from 'd3-format'; import AutoSizer from 'react-virtualized-auto-sizer'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import loadSettingRegistry from '../assets/hooks'; +import { IChartProps } from '../assets/interfaces'; -const GpuMemoryChart = (): JSX.Element => { +const GpuMemoryChart: React.FC = ({ + settingRegistry +}): JSX.Element => { const [gpuMemory, setGpuMemory] = useState([]); const [gpuTotalMemory, setGpuTotalMemory] = useState([]); + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); + + loadSettingRegistry(settingRegistry, setUpdateFrequency); useEffect(() => { async function fetchGPUMemory() { @@ -39,7 +52,7 @@ const GpuMemoryChart = (): JSX.Element => { } const intervalId = setInterval(() => { fetchGPUMemory(); - }, 1000); + }, updateFrequency); return () => clearInterval(intervalId); }, []); @@ -105,7 +118,12 @@ const GpuMemoryChart = (): JSX.Element => { }; export class GpuMemoryChartWidget extends ReactWidget { + constructor(private settingRegistry: ISettingRegistry) { + super(); + this.addClass('size-constrained-widgets'); + this.settingRegistry = settingRegistry; + } render(): JSX.Element { - return ; + return ; } } diff --git a/src/charts/GpuResourceChart.tsx b/src/charts/GpuResourceChart.tsx index 13ed299..2d251f2 100644 --- a/src/charts/GpuResourceChart.tsx +++ b/src/charts/GpuResourceChart.tsx @@ -6,10 +6,16 @@ import { requestAPI } from '../handler'; import { CustomLineChart } from '../components/customLineChart'; import { formatDate, formatBytes } from '../components/formatUtils'; import { scaleLinear } from 'd3-scale'; -import { GPU_COLOR_CATEGORICAL_RANGE } from '../assets/constants'; +import { + DEFAULT_UPDATE_FREQUENCY, + GPU_COLOR_CATEGORICAL_RANGE +} from '../assets/constants'; import { pauseIcon, playIcon } from '../assets/icons'; +import loadSettingRegistry from '../assets/hooks'; +import { IChartProps } from '../assets/interfaces'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; -interface IChartProps { +interface IDataProps { time: number; gpu_utilization_total: number; gpu_memory_total: number; @@ -19,15 +25,20 @@ interface IChartProps { gpu_memory_individual: number[]; } -const GpuResourceChart = () => { - const [gpuData, setGpuData] = useState([]); - const [tempData, setTempData] = useState([]); +const GpuResourceChart: React.FC = ({ settingRegistry }) => { + const [gpuData, setGpuData] = useState([]); + const [tempData, setTempData] = useState([]); const [isPaused, setIsPaused] = useState(false); const ngpus = gpuData[0]?.gpu_utilization_individual.length || 0; + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); + + loadSettingRegistry(settingRegistry, setUpdateFrequency); useEffect(() => { async function fetchGpuUsage() { - const response = await requestAPI('gpu_resource'); + const response = await requestAPI('gpu_resource'); if (!isPaused) { setGpuData(prevData => { if (tempData.length > 1) { @@ -42,7 +53,7 @@ const GpuResourceChart = () => { } } - const interval = setInterval(fetchGpuUsage, 1000); + const interval = setInterval(fetchGpuUsage, updateFrequency); return () => clearInterval(interval); }, [isPaused, tempData]); @@ -216,12 +227,13 @@ const GpuResourceChart = () => { }; export class GpuResourceChartWidget extends ReactWidget { - constructor() { + constructor(private settingRegistry: ISettingRegistry) { super(); /* Time series charts need to have a min height for seekbar to be visible without scrolling*/ this.addClass('size-constrained-widgets-lg'); + this.settingRegistry = settingRegistry; } - render() { - return ; + render(): JSX.Element { + return ; } } diff --git a/src/charts/GpuUtilizationChart.tsx b/src/charts/GpuUtilizationChart.tsx index f9cd5f0..cb859e6 100644 --- a/src/charts/GpuUtilizationChart.tsx +++ b/src/charts/GpuUtilizationChart.tsx @@ -12,11 +12,24 @@ import { } from 'recharts'; import { scaleLinear } from 'd3-scale'; import { renderCustomTooltip } from '../components/tooltipUtils'; -import { BAR_COLOR_LINEAR_RANGE } from '../assets/constants'; +import { + BAR_COLOR_LINEAR_RANGE, + DEFAULT_UPDATE_FREQUENCY +} from '../assets/constants'; import AutoSizer from 'react-virtualized-auto-sizer'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { IChartProps } from '../assets/interfaces'; +import loadSettingRegistry from '../assets/hooks'; -const GpuUtilizationChart = (): JSX.Element => { +const GpuUtilizationChart: React.FC = ({ + settingRegistry +}): JSX.Element => { const [gpuUtilization, setGpuUtilization] = useState([]); + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); + + loadSettingRegistry(settingRegistry, setUpdateFrequency); useEffect(() => { async function fetchGPUUtilization() { @@ -34,7 +47,7 @@ const GpuUtilizationChart = (): JSX.Element => { } const intervalId = setInterval(() => { fetchGPUUtilization(); - }, 1000); + }, updateFrequency); return () => clearInterval(intervalId); }, []); @@ -99,7 +112,13 @@ const GpuUtilizationChart = (): JSX.Element => { }; export class GpuUtilizationChartWidget extends ReactWidget { + constructor(private settingRegistry: ISettingRegistry) { + super(); + this.addClass('size-constrained-widgets'); + this.settingRegistry = settingRegistry; + } + render(): JSX.Element { - return ; + return ; } } diff --git a/src/charts/MachineResourceChart.tsx b/src/charts/MachineResourceChart.tsx index a82d517..710e7cc 100644 --- a/src/charts/MachineResourceChart.tsx +++ b/src/charts/MachineResourceChart.tsx @@ -6,10 +6,16 @@ import { requestAPI } from '../handler'; import { CustomLineChart } from '../components/customLineChart'; import { formatDate, formatBytes } from '../components/formatUtils'; import { scaleLinear } from 'd3-scale'; -import { GPU_COLOR_CATEGORICAL_RANGE } from '../assets/constants'; +import { + DEFAULT_UPDATE_FREQUENCY, + GPU_COLOR_CATEGORICAL_RANGE +} from '../assets/constants'; import { pauseIcon, playIcon } from '../assets/icons'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { IChartProps } from '../assets/interfaces'; +import loadSettingRegistry from '../assets/hooks'; -interface IChartProps { +interface IDataProps { time: number; cpu_utilization: number; memory_usage: number; @@ -23,14 +29,19 @@ interface IChartProps { network_write_current: number; } -const MachineResourceChart = () => { - const [cpuData, setCpuData] = useState([]); - const [tempData, setTempData] = useState([]); +const MachineResourceChart: React.FC = ({ settingRegistry }) => { + const [cpuData, setCpuData] = useState([]); + const [tempData, setTempData] = useState([]); const [isPaused, setIsPaused] = useState(false); + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); + + loadSettingRegistry(settingRegistry, setUpdateFrequency); useEffect(() => { async function fetchCpuUsage() { - let response = await requestAPI('cpu_resource'); + let response = await requestAPI('cpu_resource'); if (cpuData.length > 0) { response = { @@ -59,7 +70,7 @@ const MachineResourceChart = () => { } } - const interval = setInterval(fetchCpuUsage, 1000); + const interval = setInterval(fetchCpuUsage, updateFrequency); return () => clearInterval(interval); }, [isPaused, tempData]); @@ -201,12 +212,13 @@ const MachineResourceChart = () => { }; export class MachineResourceChartWidget extends ReactWidget { - constructor() { + constructor(private settingRegistry: ISettingRegistry) { super(); /* Time series charts need to have a min height for seekbar to be visible without scrolling*/ this.addClass('size-constrained-widgets-lg'); + this.settingRegistry = settingRegistry; } - render() { - return ; + render(): JSX.Element { + return ; } } diff --git a/src/charts/NvLinkThroughputChart.tsx b/src/charts/NvLinkThroughputChart.tsx index d918df9..16e19c2 100644 --- a/src/charts/NvLinkThroughputChart.tsx +++ b/src/charts/NvLinkThroughputChart.tsx @@ -5,21 +5,32 @@ import { BarChart, Bar, Cell, YAxis, XAxis, Tooltip } from 'recharts'; import { scaleLinear } from 'd3-scale'; import { renderCustomTooltip } from '../components/tooltipUtils'; import { format } from 'd3-format'; -import { BAR_COLOR_LINEAR_RANGE } from '../assets/constants'; +import { + BAR_COLOR_LINEAR_RANGE, + DEFAULT_UPDATE_FREQUENCY +} from '../assets/constants'; import AutoSizer from 'react-virtualized-auto-sizer'; +import { IChartProps } from '../assets/interfaces'; +import loadSettingRegistry from '../assets/hooks'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; -interface INvLinkChartProps { +interface IDataProps { nvlink_tx: number[]; nvlink_rx: number[]; max_rxtx_bw: number; } -const NvLinkThroughputChart = (): JSX.Element => { - const [nvlinkStats, setNvLinkStats] = useState(); +const NvLinkThroughputChart: React.FC = ({ settingRegistry }) => { + const [nvlinkStats, setNvLinkStats] = useState(); + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); + + loadSettingRegistry(settingRegistry, setUpdateFrequency); useEffect(() => { async function fetchGPUMemory() { - const response = await requestAPI('nvlink_throughput'); + const response = await requestAPI('nvlink_throughput'); console.log(response); setNvLinkStats(response); } @@ -29,12 +40,12 @@ const NvLinkThroughputChart = (): JSX.Element => { useEffect(() => { async function fetchGPUMemory() { - const response = await requestAPI('nvlink_throughput'); + const response = await requestAPI('nvlink_throughput'); setNvLinkStats(response); } const intervalId = setInterval(() => { fetchGPUMemory(); - }, 1000); + }, updateFrequency); return () => clearInterval(intervalId); }, []); @@ -138,7 +149,12 @@ const NvLinkThroughputChart = (): JSX.Element => { }; export class NvLinkThroughputChartWidget extends ReactWidget { + constructor(private settingRegistry: ISettingRegistry) { + super(); + this.addClass('size-constrained-widgets'); + this.settingRegistry = settingRegistry; + } render(): JSX.Element { - return ; + return ; } } diff --git a/src/charts/NvLinkTimelineChart.tsx b/src/charts/NvLinkTimelineChart.tsx index d32a805..0550941 100644 --- a/src/charts/NvLinkTimelineChart.tsx +++ b/src/charts/NvLinkTimelineChart.tsx @@ -7,24 +7,35 @@ import { Line, XAxis, YAxis, Brush, LineChart } from 'recharts'; import { formatDate, formatBytes } from '../components/formatUtils'; import { pauseIcon, playIcon } from '../assets/icons'; import { scaleLinear } from 'd3-scale'; -import { GPU_COLOR_CATEGORICAL_RANGE } from '../assets/constants'; +import { + DEFAULT_UPDATE_FREQUENCY, + GPU_COLOR_CATEGORICAL_RANGE +} from '../assets/constants'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { IChartProps } from '../assets/interfaces'; +import loadSettingRegistry from '../assets/hooks'; -interface INvLinkChartProps { +interface IDataProps { time: number; nvlink_tx: number[]; nvlink_rx: number[]; max_rxtx_bw: number; } -const NvLinkTimelineChart = (): JSX.Element => { - const [nvlinkStats, setNvLinkStats] = useState([]); - const [tempData, setTempData] = useState([]); +const NvLinkTimelineChart: React.FC = ({ settingRegistry }) => { + const [nvlinkStats, setNvLinkStats] = useState([]); + const [tempData, setTempData] = useState([]); const [isPaused, setIsPaused] = useState(false); const ngpus = nvlinkStats[0]?.nvlink_tx.length || 0; + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); + + loadSettingRegistry(settingRegistry, setUpdateFrequency); useEffect(() => { async function fetchNvLinkStats() { - const response = await requestAPI('nvlink_throughput'); + const response = await requestAPI('nvlink_throughput'); response.time = Date.now(); if (!isPaused) { setNvLinkStats(prevData => { @@ -40,7 +51,7 @@ const NvLinkTimelineChart = (): JSX.Element => { } } - const interval = setInterval(fetchNvLinkStats, 1000); + const interval = setInterval(fetchNvLinkStats, updateFrequency); return () => clearInterval(interval); }, [isPaused, tempData]); @@ -151,7 +162,12 @@ const NvLinkTimelineChart = (): JSX.Element => { }; export class NvLinkTimelineChartWidget extends ReactWidget { + constructor(private settingRegistry: ISettingRegistry) { + super(); + this.addClass('size-constrained-widgets'); + this.settingRegistry = settingRegistry; + } render(): JSX.Element { - return ; + return ; } } diff --git a/src/charts/PciThroughputChart.tsx b/src/charts/PciThroughputChart.tsx index b105fa8..a50bb60 100644 --- a/src/charts/PciThroughputChart.tsx +++ b/src/charts/PciThroughputChart.tsx @@ -6,19 +6,30 @@ import { scaleLinear } from 'd3-scale'; import { renderCustomTooltip } from '../components/tooltipUtils'; import AutoSizer from 'react-virtualized-auto-sizer'; import { formatBytes } from '../components/formatUtils'; -import { BAR_COLOR_LINEAR_RANGE } from '../assets/constants'; -interface IPciChartProps { +import { + BAR_COLOR_LINEAR_RANGE, + DEFAULT_UPDATE_FREQUENCY +} from '../assets/constants'; +import { IChartProps } from '../assets/interfaces'; +import loadSettingRegistry from '../assets/hooks'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +interface IDataProps { pci_tx: number[]; pci_rx: number[]; max_rxtx_tp: number; } -const PciThroughputChart = (): JSX.Element => { - const [pciStats, setPciStats] = useState(); +const PciThroughputChart: React.FC = ({ settingRegistry }) => { + const [pciStats, setPciStats] = useState(); + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); + + loadSettingRegistry(settingRegistry, setUpdateFrequency); useEffect(() => { async function fetchGPUMemory() { - const response = await requestAPI('pci_stats'); + const response = await requestAPI('pci_stats'); console.log(response); setPciStats(response); } @@ -28,12 +39,12 @@ const PciThroughputChart = (): JSX.Element => { useEffect(() => { async function fetchGPUMemory() { - const response = await requestAPI('pci_stats'); + const response = await requestAPI('pci_stats'); setPciStats(response); } const intervalId = setInterval(() => { fetchGPUMemory(); - }, 1000); + }, updateFrequency); return () => clearInterval(intervalId); }, []); @@ -133,7 +144,12 @@ const PciThroughputChart = (): JSX.Element => { }; export class PciThroughputChartWidget extends ReactWidget { + constructor(private settingRegistry: ISettingRegistry) { + super(); + this.addClass('size-constrained-widgets'); + this.settingRegistry = settingRegistry; + } render(): JSX.Element { - return ; + return ; } } diff --git a/src/index.ts b/src/index.ts index e114b6d..508913c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,44 +4,77 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ControlWidget } from './launchWidget'; import { MainAreaWidget, WidgetTracker } from '@jupyterlab/apputils'; import { gpuIcon } from './assets/icons'; +import { + COMMAND_OPEN_SETTINGS, + COMMAND_OPEN_WIDGET, + PLUGIN_ID, + PLUGIN_ID_CONFIG, + PLUGIN_ID_OPEN_SETTINGS, + WIDGET_TRACKER_NAME +} from './assets/constants'; /** * Initialization data for the react-widget extension. */ const extension: JupyterFrontEndPlugin = { - id: 'react-widget', + id: PLUGIN_ID, description: 'A minimal JupyterLab extension using a React Widget.', autoStart: true, - requires: [ILabShell], + requires: [ILabShell, ISettingRegistry], optional: [ILayoutRestorer], activate: ( app: JupyterFrontEnd, labShell: ILabShell, + settingRegistry: ISettingRegistry, restorer: ILayoutRestorer | null ) => { const tracker = new WidgetTracker({ - namespace: 'gpu-dashboard-widgets' + namespace: WIDGET_TRACKER_NAME + }); + + app.commands.addCommand(PLUGIN_ID_OPEN_SETTINGS, { + label: 'Open Settings', + execute: () => { + app.commands.execute(COMMAND_OPEN_SETTINGS, { + query: PLUGIN_ID + }); + } }); - const controlWidget = new ControlWidget(app, labShell, tracker); + + settingRegistry + .load(PLUGIN_ID_CONFIG) + .then(settings => { + console.log(`${PLUGIN_ID} settings loaded`); + }) + .catch(reason => { + console.error(`Failed to load settings for ${PLUGIN_ID}.`, reason); + }); + + const controlWidget = new ControlWidget( + app, + labShell, + tracker, + settingRegistry + ); controlWidget.id = 'gpu-dashboard'; controlWidget.title.icon = gpuIcon; controlWidget.title.caption = 'GPU Dashboards'; - // If there is a restorer, restore the widget if (restorer) { // Add state restoration for the widget so they can be restored on reload restorer.add(controlWidget, 'gpu-dashboard'); // Track and restore the chart widgets states so they can be restored on reload restorer.restore(tracker, { - command: 'gpu-dashboard-widget:open', + command: COMMAND_OPEN_WIDGET, args: widget => ({ id: widget.id, title: widget.title.label }), name: widget => widget.title.label }); } - + // If there is a restorer, restore the widget // Add control widget to the left area labShell.add(controlWidget, 'left', { rank: 200 }); } diff --git a/src/launchWidget.tsx b/src/launchWidget.tsx index 6633d0d..9342f3b 100644 --- a/src/launchWidget.tsx +++ b/src/launchWidget.tsx @@ -1,6 +1,11 @@ -import React from 'react'; +import React, { useState } from 'react'; import { ILabShell, JupyterFrontEnd } from '@jupyterlab/application'; -import { ReactWidget, Button, LabIcon } from '@jupyterlab/ui-components'; +import { + ReactWidget, + Button, + LabIcon, + settingsIcon +} from '@jupyterlab/ui-components'; import { GpuResourceChartWidget, GpuMemoryChartWidget, @@ -12,33 +17,40 @@ import { } from './charts'; import { MainAreaWidget, WidgetTracker } from '@jupyterlab/apputils'; import { gpuIcon, hBarIcon, vBarIcon, lineIcon } from './assets/icons'; - -interface IControlProps { - app: JupyterFrontEnd; - labShell: ILabShell; - tracker: WidgetTracker; -} - -export interface IWidgetInfo { - id: string; - title: string; - instance: MainAreaWidget; -} +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { IControlProps, IWidgetInfo } from './assets/interfaces'; +import { + COMMAND_OPEN_WIDGET, + DEFAULT_UPDATE_FREQUENCY, + PLUGIN_ID_OPEN_SETTINGS +} from './assets/constants'; +import loadSettingRegistry from './assets/hooks'; // Control component for the GPU Dashboard, which contains buttons to open the GPU widgets -const Control: React.FC = ({ app, labShell, tracker }) => { +const Control: React.FC = ({ + app, + labShell, + tracker, + settingRegistry +}) => { // Keep track of open widgets const openWidgets: IWidgetInfo[] = []; - console.log(tracker); + const [updateFrequency, setUpdateFrequency] = useState( + DEFAULT_UPDATE_FREQUENCY + ); - // Add command to open GPU Dashboard Widget - app.commands.addCommand('gpu-dashboard-widget:open', { - label: 'Open GPU Dashboard Widget', - execute: args => { - const { id, title } = args as { id: string; title: string }; - openWidgetById(id, title); - } - }); + loadSettingRegistry(settingRegistry, setUpdateFrequency); + + if (!app.commands.hasCommand(COMMAND_OPEN_WIDGET)) { + // Add command to open GPU Dashboard Widget + app.commands.addCommand(COMMAND_OPEN_WIDGET, { + label: 'Open GPU Dashboard Widget', + execute: args => { + const { id, title } = args as { id: string; title: string }; + openWidgetById(id, title); + } + }); + } /* Function to create a widget by id and title and add it to the main area, or bring it to the front if it is already open */ @@ -60,9 +72,9 @@ const Control: React.FC = ({ app, labShell, tracker }) => { const content = widgetCreator(); const widgetInstance = new MainAreaWidget({ content }); widgetInstance.title.label = title; + widgetInstance.title.caption = title; widgetInstance.title.icon = gpuIcon; widgetInstance.id = id; - widgetInstance.addClass('size-constrained-widgets'); app.shell.add(widgetInstance, 'main'); tracker.add(widgetInstance); openWidgets.push({ id, title, instance: widgetInstance }); @@ -83,25 +95,25 @@ const Control: React.FC = ({ app, labShell, tracker }) => { let widgetFunction; switch (id) { case 'gpu-memory-widget': - widgetFunction = () => new GpuMemoryChartWidget(); + widgetFunction = () => new GpuMemoryChartWidget(settingRegistry); break; case 'gpu-utilization-widget': - widgetFunction = () => new GpuUtilizationChartWidget(); + widgetFunction = () => new GpuUtilizationChartWidget(settingRegistry); break; case 'gpu-resource-widget': - widgetFunction = () => new GpuResourceChartWidget(); + widgetFunction = () => new GpuResourceChartWidget(settingRegistry); break; case 'machine-resource-widget': - widgetFunction = () => new MachineResourceChartWidget(); + widgetFunction = () => new MachineResourceChartWidget(settingRegistry); break; case 'pci-throughput-widget': - widgetFunction = () => new PciThroughputChartWidget(); + widgetFunction = () => new PciThroughputChartWidget(settingRegistry); break; case 'nvlink-throughput-widget': - widgetFunction = () => new NvLinkThroughputChartWidget(); + widgetFunction = () => new NvLinkThroughputChartWidget(settingRegistry); break; case 'nvlink-throughput-timeseries-widget': - widgetFunction = () => new NvLinkTimelineChartWidget(); + widgetFunction = () => new NvLinkTimelineChartWidget(settingRegistry); break; default: return; @@ -120,7 +132,15 @@ const Control: React.FC = ({ app, labShell, tracker }) => { return (
-
GPU Dashboards
+
+ GPU Dashboards + +

+
+
+ + Updated every {updateFrequency}ms + +
); }; @@ -185,15 +211,22 @@ export class ControlWidget extends ReactWidget { constructor( private app: JupyterFrontEnd, private labShell: ILabShell, - private tracker: WidgetTracker + private tracker: WidgetTracker, + private settingRegistry: ISettingRegistry ) { super(); this.tracker = tracker; + this.settingRegistry = settingRegistry; } render(): JSX.Element { return ( - + ); } } diff --git a/style/base.css b/style/base.css index effb8ab..364ef30 100644 --- a/style/base.css +++ b/style/base.css @@ -53,13 +53,25 @@ } .gpu-dashboard-header { + display: flex; /* Use flexbox layout */ + align-items: center; /* Align items vertically */ + justify-content: space-between; /* Space evenly between items */ font-size: 25px; /* Adjust font size */ font-weight: bold; margin-bottom: 10px; - align-self: flex-start; /* Left align the header */ color: #ff7900; } +.gpu-dashboard-footer { + font-size: 18px; + color: #ff7900; + margin-top: 30px; +} + +.gpu-dashboard-footer-body { + font-variant: petite-caps; +} + .gpu-dashboard-divider { width: 100%; border: none; @@ -67,6 +79,13 @@ margin: -8px -5px 10px 0; } +.header-text { + align-self: flex-start; /* Align text to the left */ +} +.header-button { + background: transparent; +} + .gpu-dashboard-button { margin: 5px 0; padding: 10px 20px; @@ -128,6 +147,14 @@ height: 40px; } +.nv-header-icon:hover { + stroke: #ff7900; +} + +.nv-header-icon svg path { + fill: #ff7900 !important; +} + .nv-icon-custom { stroke: #ff7900; fill: #ff7900; diff --git a/yarn.lock b/yarn.lock index 9ade321..001bbc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -439,6 +439,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.1.0": + version: 4.1.0 + resolution: "@jupyterlab/nbformat@npm:4.1.0" + dependencies: + "@lumino/coreutils": ^2.1.2 + checksum: 0f10f53d312e1ad386be0cd1db3ea8d76ac5e169a1c470465179b35c7d7bd0e55b9d450b64abe38f447dcbec71224bfe8d4115a1cdb433f986d3a91234ffd391 + languageName: node + linkType: hard + "@jupyterlab/observables@npm:^5.0.6": version: 5.0.6 resolution: "@jupyterlab/observables@npm:5.0.6" @@ -520,6 +529,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/settingregistry@npm:^4.1.0": + version: 4.1.0 + resolution: "@jupyterlab/settingregistry@npm:4.1.0" + dependencies: + "@jupyterlab/nbformat": ^4.1.0 + "@jupyterlab/statedb": ^4.1.0 + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + "@rjsf/utils": ^5.13.4 + ajv: ^8.12.0 + json5: ^2.2.3 + peerDependencies: + react: ">=16" + checksum: 1a0c52016806ceda150168cdeae966b15afce454fe24acfd68939f3f380eaf2d4390c40e27c1475877c8e8da6b3f15a952999ebcc9d3838d5306bd24ad5b4b51 + languageName: node + linkType: hard + "@jupyterlab/statedb@npm:^4.0.6": version: 4.0.6 resolution: "@jupyterlab/statedb@npm:4.0.6" @@ -533,6 +561,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statedb@npm:^4.1.0": + version: 4.1.0 + resolution: "@jupyterlab/statedb@npm:4.1.0" + dependencies: + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + checksum: 693d40ba6ce67b41aae2acbae027a5c637c2bfa51d7085b6faecdb1877a5e3bd43ca70f3670f88f038c49bef80e0e09899b05d330dd9010b1d578ca73b13ea17 + languageName: node + linkType: hard + "@jupyterlab/statusbar@npm:^4.0.6": version: 4.0.6 resolution: "@jupyterlab/statusbar@npm:4.0.6" @@ -633,6 +674,21 @@ __metadata: languageName: node linkType: hard +"@lumino/commands@npm:^2.2.0": + version: 2.2.0 + resolution: "@lumino/commands@npm:2.2.0" + dependencies: + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/domutils": ^2.0.1 + "@lumino/keyboard": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/virtualdom": ^2.0.1 + checksum: 093e9715491e5cef24bc80665d64841417b400f2fa595f9b60832a3b6340c405c94a6aa276911944a2c46d79a6229f3cc087b73f50852bba25ece805abd0fae9 + languageName: node + linkType: hard + "@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.1.2, @lumino/coreutils@npm:^2.1.2": version: 2.1.2 resolution: "@lumino/coreutils@npm:2.1.2" @@ -818,6 +874,21 @@ __metadata: languageName: node linkType: hard +"@rjsf/utils@npm:^5.13.4": + version: 5.17.0 + resolution: "@rjsf/utils@npm:5.17.0" + dependencies: + json-schema-merge-allof: ^0.8.1 + jsonpointer: ^5.0.1 + lodash: ^4.17.21 + lodash-es: ^4.17.21 + react-is: ^18.2.0 + peerDependencies: + react: ^16.14.0 || >=17 + checksum: 01d0001f83083764a8552e009aa7df084621df9d1fc6ccdfad9d534513084421b1ad7494cab77b9b8205d680fd915f612d87800e20ab242e7066f33184c73d4f + languageName: node + linkType: hard + "@types/d3-array@npm:^3.0.3": version: 3.0.8 resolution: "@types/d3-array@npm:3.0.8" @@ -3590,6 +3661,7 @@ __metadata: "@jupyterlab/coreutils": ^6.0.0 "@jupyterlab/launcher": ^4.0.5 "@jupyterlab/services": ^7.0.0 + "@jupyterlab/settingregistry": ^4.1.0 "@types/d3-format": ^3.0.1 "@types/d3-scale": ^4.0.4 "@types/json-schema": ^7.0.11