diff --git a/docs/source/user_guide.ipynb b/docs/source/user_guide.ipynb index b399438..4bc6590 100644 --- a/docs/source/user_guide.ipynb +++ b/docs/source/user_guide.ipynb @@ -819,6 +819,57 @@ "\n", "If you use a replacement server (e.g. jupyverse) you will need to add the required header manually or switch to the default jupyter-server for the time of profiling." ] + }, + { + "cell_type": "markdown", + "id": "73052b6b-24d6-4b91-812e-254a61f2ba90", + "metadata": {}, + "source": [ + "## Custom scenarios" + ] + }, + { + "cell_type": "markdown", + "id": "de361f64-5913-453f-8603-8624c92bf571", + "metadata": {}, + "source": [ + "### Programmatic scenarios\n", + "\n", + "You can define custom scenarios programmatically by creating a JupyterLab extension which consumes `IUIProfiler` token:\n", + "\n", + "```typescript\n", + "import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';\n", + "import { IScenario, IUIProfiler } from '@jupyterlab/ui-profiler';\n", + "\n", + "\n", + "class MyScenario implements IScenario {\n", + " id = 'myScenario';\n", + " name = 'My scenario';\n", + "\n", + " async run(): Promise {\n", + " console.log('Running!');\n", + " }\n", + "}\n", + "\n", + "export const plugin: JupyterFrontEndPlugin = {\n", + " id: '@my-organization/my-ui-profiler-extension:my-scenario',\n", + " autoStart: true,\n", + " requires: [IUIProfiler],\n", + " activate: (app: JupyterFrontEnd, profiler: IUIProfiler) => {\n", + " const myScenario = new MyScenario();\n", + " profiler.addScenario(myScenario);\n", + " }\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "f41ca671-fa3b-4aec-9985-7d2b1116d93c", + "metadata": {}, + "source": [ + "Please see [`tokens.ts` file](https://github.com/jupyterlab/ui-profiler/blob/main/src/tokens.ts) for the full documentation of `IScenario` interface." + ] } ], "metadata": { diff --git a/src/benchmark.ts b/src/benchmark.ts index d243f7a..51a6af9 100644 --- a/src/benchmark.ts +++ b/src/benchmark.ts @@ -6,21 +6,7 @@ import { layoutReady } from './dramaturg'; import benchmarkExecutionOptionsSchema from './schema/benchmark-execution.json'; import type { ExecutionTimeBenchmarkOptions } from './types/_benchmark-execution'; import { renderTimings } from './ui'; - -export interface IScenario { - id: string; - name: string; - run: () => Promise; - - setupSuite?: () => Promise; - cleanupSuite?: () => Promise; - - setup?: () => Promise; - cleanup?: () => Promise; - - configSchema: JSONSchema7; - setOptions: (options: any) => void; -} +import { IScenario } from './tokens'; export interface IProgress { percentage: number; diff --git a/src/index.ts b/src/index.ts index 4f33dd2..dc02381 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,16 +19,6 @@ import { styleRuleUsageBenchmark } from './styleBenchmarks'; import { selfProfileBenchmark } from './jsBenchmarks'; -import { - MenuOpenScenario, - MenuSwitchScenario, - SwitchTabScenario, - SwitchTabFocusScenario, - SidebarOpenScenario, - CompleterScenario, - ScrollScenario, - DebuggerScenario -} from './scenarios'; import { executionTimeBenchmark, IBenchmark, @@ -36,6 +26,8 @@ import { IProfilingOutcome } from './benchmark'; import { IJupyterState } from './utils'; +import { IScenario, IUIProfiler } from './tokens'; +import { plugin as scenariosPlugin } from './scenarios'; namespace CommandIDs { // export const findUnusedStyles = 'ui-profiler:find-unused-styles'; @@ -45,11 +37,12 @@ namespace CommandIDs { /** * Initialization data for the @jupyterlab/ui-profiler extension. */ -const plugin: JupyterFrontEndPlugin = { +const plugin: JupyterFrontEndPlugin = { id: '@jupyterlab/ui-profiler:plugin', autoStart: true, requires: [IDocumentManager], optional: [ILauncher, ILayoutRestorer], + provides: IUIProfiler, activate: ( app: JupyterFrontEnd, docManager: IDocumentManager, @@ -59,6 +52,7 @@ const plugin: JupyterFrontEndPlugin = { const fileBrowserModel = new FileBrowserModel({ manager: docManager }); + const scenarios: IScenario[] = []; const options = { benchmarks: [ executionTimeBenchmark, @@ -68,16 +62,7 @@ const plugin: JupyterFrontEndPlugin = { styleRuleUsageBenchmark, selfProfileBenchmark ] as (IBenchmark> | IBenchmark)[], - scenarios: [ - new MenuOpenScenario(app), - new MenuSwitchScenario(app), - new SwitchTabScenario(app), - new SwitchTabFocusScenario(app), - new SidebarOpenScenario(app), - new CompleterScenario(app), - new ScrollScenario(app), - new DebuggerScenario(app) - ], + scenarios: scenarios, translator: nullTranslator, upload: (file: File) => { // https://github.com/jupyterlab/jupyterlab/issues/11416 @@ -156,7 +141,18 @@ const plugin: JupyterFrontEndPlugin = { rank: 1 }); } + + return { + addScenario: (scenario: IScenario) => { + scenarios.push(scenario); + if (lastWidget) { + lastWidget.content.addScenario(scenario); + } + } + }; } }; -export default plugin; +export * from './tokens'; + +export default [plugin, scenariosPlugin]; diff --git a/src/jsBenchmarks.ts b/src/jsBenchmarks.ts index 21b6ba2..775be5f 100644 --- a/src/jsBenchmarks.ts +++ b/src/jsBenchmarks.ts @@ -1,7 +1,6 @@ import { JSONSchema7 } from 'json-schema'; import { - IScenario, IProfilingOutcome, IBenchmark, IProfileMeasurement, @@ -10,6 +9,7 @@ import { import { reportTagCounts } from './utils'; import { layoutReady } from './dramaturg'; import { renderProfile } from './ui'; +import { IScenario } from './tokens'; import benchmarkProfileOptionsSchema from './schema/benchmark-profile.json'; import type { ProfileBenchmarkOptions } from './types/_benchmark-profile'; diff --git a/src/scenarios.ts b/src/scenarios.ts index 9850a75..4fbc940 100644 --- a/src/scenarios.ts +++ b/src/scenarios.ts @@ -1,8 +1,10 @@ -import type { JupyterFrontEnd } from '@jupyterlab/application'; +import type { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; import type { MainAreaWidget } from '@jupyterlab/apputils'; import { JSONSchema7 } from 'json-schema'; - import { page, layoutReady, @@ -10,10 +12,9 @@ import { waitForScrollEnd, waitUntilDisappears } from './dramaturg'; -import { IScenario } from './benchmark'; +import { IScenario, IUIProfiler } from './tokens'; import type { TabScenarioOptions, Tab } from './types/_scenario-tabs'; -import type { ScenarioOptions } from './types/_scenario-base'; import type { MenuOpenScenarioOptions } from './types/_scenario-menu-open'; import type { CompleterScenarioOptions } from './types/_scenario-completer'; import type { SidebarsScenarioOptions } from './types/_scenario-sidebars'; @@ -53,10 +54,6 @@ export class MenuSwitchScenario implements IScenario { // no-op } - setOptions(options: ScenarioOptions): void { - // no-op - } - async setup(): Promise { return openMainMenu(this.jupyterApp); } @@ -597,3 +594,21 @@ export class SwitchTabFocusScenario extends SwitchTabScenario { name = 'Switch Tab Focus'; split: 'first' | 'all' = 'all'; } + +export const plugin: JupyterFrontEndPlugin = { + id: '@jupyterlab/ui-profiler:default-scenarios', + autoStart: true, + requires: [IUIProfiler], + activate: (app: JupyterFrontEnd, profiler: IUIProfiler) => { + [ + new MenuOpenScenario(app), + new MenuSwitchScenario(app), + new SwitchTabScenario(app), + new SwitchTabFocusScenario(app), + new SidebarOpenScenario(app), + new CompleterScenario(app), + new ScrollScenario(app), + new DebuggerScenario(app) + ].map(scenario => profiler.addScenario(scenario)); + } +}; diff --git a/src/styleBenchmarks.tsx b/src/styleBenchmarks.tsx index 3cd09b5..e213fc2 100644 --- a/src/styleBenchmarks.tsx +++ b/src/styleBenchmarks.tsx @@ -2,7 +2,6 @@ import { JSONSchema7 } from 'json-schema'; import React from 'react'; import { - IScenario, ITimeMeasurement, ITimingOutcome, IBenchmark, @@ -17,6 +16,7 @@ import { collectRules } from './css'; import { renderBlockResult } from './ui'; +import { IScenario } from './tokens'; import benchmarkOptionsSchema from './schema/benchmark-base.json'; import benchmarkRuleOptionsSchema from './schema/benchmark-rule.json'; diff --git a/src/tokens.ts b/src/tokens.ts new file mode 100644 index 0000000..f83b2b4 --- /dev/null +++ b/src/tokens.ts @@ -0,0 +1,73 @@ +import { Token } from '@lumino/coreutils'; +import { JSONSchema7 } from 'json-schema'; + +/** + * Scenario defining set of steps to carry out during benchmarking. + */ +export interface IScenario { + /** + * The internal identifier, has to be unique. + */ + id: string; + + /** + * The name displayed to user. + */ + name: string; + + /** + * The actual scenario execution. + * + * Note: any async calls made in the `run()` function should be awaited so + * that the execution time measurements are accurate and to prevent calling + * `cleanup()` too early. + */ + run: () => Promise; + + /** + * Prepare scenario before running, called once for any given benchmark run. + */ + setupSuite?: () => Promise; + + /** + * Clean up after scenario running, called once for any given benchmark run. + */ + cleanupSuite?: () => Promise; + + /** + * Prepare for scenario repeat, called as many times as benchmark repeats. + */ + setup?: () => Promise; + + /** + * Clean up after scenario, called as many times as benchmark repeats. + */ + cleanup?: () => Promise; + + /** + * Configuration schema used to build the configuration form with rjsf. + */ + configSchema?: JSONSchema7; + + /** + * Callback receiving JSON object with user configuration choices. + */ + setOptions?: (options: any) => void; +} + +/** + * The UIProfiler public API. + */ +export interface IUIProfiler { + /** + * Add scenario to profiler. + */ + addScenario(scenario: IScenario): void; +} + +/** + * The UIProfiler token. + */ +export const IUIProfiler = new Token( + '@jupyterlab/ui-profiler:manager' +); diff --git a/src/ui.tsx b/src/ui.tsx index f124778..18405dd 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -12,7 +12,6 @@ import { IBenchmark, IOutcome, ITimeMeasurement, - IScenario, IProgress, ITimingOutcome, IProfilingOutcome @@ -28,6 +27,7 @@ import { import { Statistic } from './statistics'; import { TimingTable, ResultTable } from './table'; import { LuminoWidget } from './lumino'; +import { IScenario, IUIProfiler } from './tokens'; interface IProfilerProps { benchmarks: (IBenchmark | IBenchmark)[]; @@ -604,7 +604,7 @@ export function renderBlockResult(props: { ); } -export class UIProfiler extends ReactWidget { +export class UIProfiler extends ReactWidget implements IUIProfiler { constructor(private props: IProfilerProps) { super(); this.progress = new Signal(this); @@ -627,6 +627,11 @@ export class UIProfiler extends ReactWidget { this.handleResult(JSON.parse(file.content)); } + addScenario(scenario: IScenario) { + this.props.scenarios.push(scenario); + this.update(); + } + render(): JSX.Element { return (
@@ -1038,8 +1043,8 @@ export class BenchmarkLauncher extends React.Component< super(props); this._stop = new Signal(this); this.state = { - benchmarks: [props.benchmarks[0]], - scenarios: [props.scenarios[0]], + benchmarks: props.benchmarks.length !== 0 ? [props.benchmarks[0]] : [], + scenarios: props.scenarios.length !== 0 ? [props.scenarios[0]] : [], fieldTemplate: CustomTemplateFactory(this.props.translator), arrayFieldTemplate: CustomArrayTemplateFactory(this.props.translator), objectFieldTemplate: CustomObjectTemplateFactory(this.props.translator), @@ -1059,7 +1064,9 @@ export class BenchmarkLauncher extends React.Component< scenario: config.scenarios[scenario.id], benchmark: config.benchmarks[benchmark.id] } as any); - scenario.setOptions(options.scenario); + if (scenario.setOptions) { + scenario.setOptions(options.scenario); + } this.props.progress.emit({ percentage: 0 }); const result = (await benchmark.run( scenario, @@ -1272,8 +1279,12 @@ export class BenchmarkLauncher extends React.Component<
{this.state.scenarios.map(scenario => { - const properties = scenario.configSchema.properties; - if (!properties || Object.keys(properties).length === 0) { + const properties = scenario.configSchema?.properties; + if ( + !scenario.configSchema || + !properties || + Object.keys(properties).length === 0 + ) { return ; } scenario.configSchema.title = scenario.name + ' configuration';