diff --git a/integration/standalone/src/app.ts b/integration/standalone/src/app.ts index b6909c2d..4f456b3c 100644 --- a/integration/standalone/src/app.ts +++ b/integration/standalone/src/app.ts @@ -23,6 +23,7 @@ const sourceUri = parameters.get('file') ?? ''; const select = parameters.get('select'); const theme = (parameters.get('theme') as ThemeMode) ?? SwitchThemeActionHandler.prefsColorScheme(); const debug = parameters.has('debug', 'true'); +const measurePerformance = parameters.has('performance', 'true'); const id = 'ivy-glsp-process-editor'; const diagramType = 'ivy-glsp-process'; @@ -51,7 +52,8 @@ async function initialize(connectionProvider: MessageConnection, isReconnecting app, pmv, server: webSocketBase - } + }, + measurePerformance }); const diagramLoader = container.get(DiagramLoader); diff --git a/integration/standalone/src/di.config.ts b/integration/standalone/src/di.config.ts index c0ae0628..e668022d 100644 --- a/integration/standalone/src/di.config.ts +++ b/integration/standalone/src/di.config.ts @@ -1,4 +1,4 @@ -import { createIvyDiagramContainer, ivyThemeModule } from '@axonivy/process-editor'; +import { createIvyDiagramContainer, createPerformanceModule, ivyThemeModule } from '@axonivy/process-editor'; import { ivyInscriptionModule } from '@axonivy/process-editor-inscription'; import type { IDiagramOptions } from '@eclipse-glsp/client'; import { createDiagramOptionsModule, standaloneExportModule, standaloneSelectModule, undoRedoModule } from '@eclipse-glsp/client'; @@ -15,12 +15,14 @@ export interface IvyDiagramOptions extends IDiagramOptions { select: string | null; theme: ThemeMode; inscriptionContext: InscriptionContext & { server: string }; + measurePerformance?: boolean; } export default function createContainer(options: IvyDiagramOptions): Container { const container = createIvyDiagramContainer( 'sprotty', createDiagramOptionsModule(options), + createPerformanceModule(options.measurePerformance), // standalone modules standaloneSelectModule, standaloneExportModule, diff --git a/integration/viewer/src/app.ts b/integration/viewer/src/app.ts index ed174130..80d07056 100644 --- a/integration/viewer/src/app.ts +++ b/integration/viewer/src/app.ts @@ -23,6 +23,7 @@ const highlight = parameters.get('highlight') ?? ''; const select = parameters.get('select'); const zoom = parameters.get('zoom') ?? ''; const theme = (parameters.get('theme') as ThemeMode) ?? SwitchThemeActionHandler.prefsColorScheme(); +const measurePerformance = parameters.has('performance', 'true'); const id = 'ivy-glsp-process-viewer'; const diagramType = 'ivy-glsp-process'; @@ -47,7 +48,8 @@ async function initialize(connectionProvider: MessageConnection, isReconnecting highlight, select, zoom, - theme + theme, + measurePerformance }); const diagramLoader = container.get(DiagramLoader); diff --git a/integration/viewer/src/di.config.ts b/integration/viewer/src/di.config.ts index 89515092..f0e51970 100644 --- a/integration/viewer/src/di.config.ts +++ b/integration/viewer/src/di.config.ts @@ -1,5 +1,6 @@ import { createIvyDiagramContainer, + createPerformanceModule, ivyChangeBoundsToolModule, ivyConnectorModule, ivyKeyListenerModule, @@ -26,6 +27,7 @@ export interface IvyDiagramOptions extends IDiagramOptions { select: string | null; zoom: string; theme: ThemeMode; + measurePerformance?: boolean; } export default function createContainer(options: IvyDiagramOptions): Container { @@ -33,6 +35,7 @@ export default function createContainer(options: IvyDiagramOptions): Container { const container = createIvyDiagramContainer( 'sprotty', createDiagramOptionsModule(options), + createPerformanceModule(options.measurePerformance), ivyThemeModule, ivyNavigationModule, ivyStartupDiagramModule, diff --git a/packages/editor/src/di.config.ts b/packages/editor/src/di.config.ts index d9d0b9b2..9a3ff890 100644 --- a/packages/editor/src/di.config.ts +++ b/packages/editor/src/di.config.ts @@ -25,7 +25,6 @@ import ivyDiagramModule from './diagram/di.config'; import { LaneNode } from './diagram/model'; import { ivyLabelEditModule, ivyLabelEditUiModule } from './edit-label/di.config'; import ivyExecutionModule from './execution/di.config'; -import { IvyGLSPCommandStack } from './ivy-command-stack'; import ivyJumpModule from './jump/di.config'; import ivyKeyListenerModule from './key-listener/di.config'; import ivyLaneModule from './lanes/di.config'; @@ -93,7 +92,7 @@ export default function createContainer(widgetId: string, ...containerConfigurat bindOrRebind(container, MarqueeUtil).to(IvyMarqueeUtil).inSingletonScope(); bindOrRebind(container, TYPES.IMarqueeBehavior).toConstantValue({ entireEdge: true, entireElement: true }); - bindOrRebind(container, TYPES.ICommandStack).to(IvyGLSPCommandStack).inSingletonScope(); + // bindOrRebind(container, TYPES.ICommandStack).to(IvyGLSPCommandStack).inSingletonScope(); bindOrRebind(container, TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); bindOrRebind(container, TYPES.LogLevel).toConstantValue(LogLevel.warn); bindOrRebind(container, TYPES.ISnapper).to(GLSPCenterGridSnapper); diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index de13b466..911e3e2a 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -42,5 +42,5 @@ export * from './ui-tools/viewport/viewport-bar'; export * from './key-listener/jump-out'; export * from './key-listener/quick-actions'; export * from './start-action/actions'; - +export * from './performance/performance-module'; export * from './ivy-glsp-jsonrpc-client'; diff --git a/packages/editor/src/performance/perf-action-dispatcher.ts b/packages/editor/src/performance/perf-action-dispatcher.ts new file mode 100644 index 00000000..1747c104 --- /dev/null +++ b/packages/editor/src/performance/perf-action-dispatcher.ts @@ -0,0 +1,25 @@ +import { Action, GLSPActionDispatcher, RequestAction, ResponseAction } from '@eclipse-glsp/client'; +import { injectable } from 'inversify'; + +@injectable() +export class PerfActionDispatcher extends GLSPActionDispatcher { + protected counter = 0; + + override request(action: RequestAction): Promise { + const counter = ++this.counter; + console.time(`request-${action.kind}-${counter}`); + const result = super.request(action); + console.timeEnd(`request-${action.kind}-${counter}`); + this.counter++; + return result; + } + + protected override async handleAction(action: Action): Promise { + const counter = ++this.counter; + console.time(`handleAction-${action.kind}-${counter}`); + console.log(`handleAction-${action.kind}-${counter}`, action); + const result = await super.handleAction(action); + console.timeEnd(`handleAction-${action.kind}-${counter}`); + return result; + } +} diff --git a/packages/editor/src/performance/perf-command-stack.ts b/packages/editor/src/performance/perf-command-stack.ts new file mode 100644 index 00000000..905454be --- /dev/null +++ b/packages/editor/src/performance/perf-command-stack.ts @@ -0,0 +1,19 @@ +import { GModelRoot, ICommand } from '@eclipse-glsp/client'; +import { injectable } from 'inversify'; +import { IvyGLSPCommandStack } from '../ivy-command-stack'; + +@injectable() +export class PerfCommandStack extends IvyGLSPCommandStack { + protected counter = 0; + + override async execute(command: ICommand): Promise { + const counter = ++this.counter; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const kind = (command as any).action?.kind; + const perfmessage = kind ? `executeCommand-${kind}-${counter}` : `executeCommand-${counter}`; + console.time(perfmessage); + const result = await super.execute(command); + console.timeEnd(perfmessage); + return result; + } +} diff --git a/packages/editor/src/performance/perf-diagram-loader.ts b/packages/editor/src/performance/perf-diagram-loader.ts new file mode 100644 index 00000000..87566693 --- /dev/null +++ b/packages/editor/src/performance/perf-diagram-loader.ts @@ -0,0 +1,54 @@ +import { DiagramLoader, DiagramLoadingOptions, IDiagramStartup, ResolvedDiagramLoadingOptions, StatusAction } from '@eclipse-glsp/client'; +import { injectable } from 'inversify'; + +@injectable() +export class PerfDiagramLoader extends DiagramLoader { + protected override async invokeStartupHook(hook: keyof Omit): Promise { + console.time('invokeStartupHook-' + hook); + await super.invokeStartupHook(hook); + console.timeEnd('invokeStartupHook-' + hook); + } + + protected override async initialize(options: ResolvedDiagramLoadingOptions): Promise { + console.time('DiagramLoader.initialize (DI)'); + if (options.enableNotifications) { + console.time('dispatchStatus - (DI)'); + await this.actionDispatcher.dispatch(StatusAction.create('Initializing...', { severity: 'INFO' })); + console.timeEnd('dispatchStatus - (DI)'); + } + + console.time('getClient - (DI)'); + const glspClient = await this.options.glspClientProvider(); + console.timeEnd('getClient - (DI)'); + console.time('startClient - (DI)'); + await glspClient.start(); + console.timeEnd('startClient - (DI)'); + if (!glspClient.initializeResult) { + console.time('initializeServer - (DI)'); + await glspClient.initializeServer(options.initializeParameters); + console.timeEnd('initializeServer - (DI)'); + } + console.time('configureModelSource - (DI)'); + this.modelSource.configure(glspClient); + console.timeEnd('configureModelSource - (DI)'); + + if (options.enableNotifications) { + console.time('clearStatus - (DI)'); + this.actionDispatcher.dispatch(StatusAction.create('', { severity: 'NONE' })); + console.timeEnd('clearStatus - (DI)'); + } + console.timeEnd('DiagramLoader.initialize (DI)'); + } + + protected override async requestModel(options: ResolvedDiagramLoadingOptions): Promise { + console.time('DiagramLoader.requestModel'); + await super.requestModel(options); + console.timeEnd('DiagramLoader.requestModel'); + } + + override async load(options: DiagramLoadingOptions = {}): Promise { + console.time('DiagramLoader.load'); + await super.load(options); + console.timeEnd('DiagramLoader.load'); + } +} diff --git a/packages/editor/src/performance/perf-model-source.ts b/packages/editor/src/performance/perf-model-source.ts new file mode 100644 index 00000000..4c55c9fc --- /dev/null +++ b/packages/editor/src/performance/perf-model-source.ts @@ -0,0 +1,32 @@ +import { Disposable, GLSPClient, GLSPModelSource } from '@eclipse-glsp/client'; +import { injectable } from 'inversify'; + +@injectable() +export class PerfGLSPModelSource extends GLSPModelSource { + async configure(glspClient: GLSPClient): Promise { + console.time('GLSPModelSource.configure (MS)'); + this.glspClient = glspClient; + if (!glspClient.initializeResult) { + throw new Error('Could not configure model source. The GLSP client is not initialized yet!'); + } + + console.time('createInitializeClientSessionParameters (MS)'); + const initializeParams = this.createInitializeClientSessionParameters(glspClient.initializeResult); + console.timeEnd('createInitializeClientSessionParameters (MS)'); + + console.time('configureServerActions (MS)'); + this.configureServeActions(glspClient.initializeResult); + console.timeEnd('configureServerActions (MS)'); + + this.toDispose.push( + glspClient.onActionMessage(message => this.messageReceived(message), this.clientId), + Disposable.create(() => glspClient.disposeClientSession(this.createDisposeClientSessionParameters())) + ); + + console.time('initializeClientSession (MS)'); + const result = await glspClient!.initializeClientSession(initializeParams); + console.timeEnd('initializeClientSession (MS)'); + console.timeEnd('GLSPModelSource.configure (MS)'); + return result; + } +} diff --git a/packages/editor/src/performance/perf-viewer.tsx b/packages/editor/src/performance/perf-viewer.tsx new file mode 100644 index 00000000..df5f1b5d --- /dev/null +++ b/packages/editor/src/performance/perf-viewer.tsx @@ -0,0 +1,45 @@ +/** @jsx html */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Action, GModelRoot, ModelViewer, copyClassesFromElement, copyClassesFromVNode, html, setClass } from '@eclipse-glsp/client'; +import { injectable } from 'inversify'; + +@injectable() +export class PerfModelViewer extends ModelViewer { + protected counter = 0; + update(model: Readonly, cause?: Action): void { + const counter = ++this.counter; + console.time(`Viewer update (vu) (${counter})`); + this.logger.log(this, 'rendering', model); + const newVDOM =
{this.renderer.renderElement(model)}
; + if (this.lastVDOM !== undefined) { + const hadFocus = this.hasFocus(); + copyClassesFromVNode(this.lastVDOM, newVDOM); + this.lastVDOM = this.patcher.call(this, this.lastVDOM, newVDOM); + this.restoreFocus(hadFocus); + } else if (typeof document !== 'undefined') { + let placeholder = null; + if (this.options.shadowRoot) { + const shadowRoot = document.getElementById(this.options.shadowRoot)?.shadowRoot; + if (shadowRoot) { + placeholder = shadowRoot.getElementById(this.options.baseDiv); + } + } else { + placeholder = document.getElementById(this.options.baseDiv); + } + if (placeholder !== null) { + if (typeof window !== 'undefined') { + window.addEventListener('resize', () => { + this.onWindowResize(newVDOM); + }); + } + copyClassesFromElement(placeholder, newVDOM); + setClass(newVDOM, this.options.baseClass, true); + this.lastVDOM = this.patcher.call(this, placeholder, newVDOM); + } else { + this.logger.error(this, 'element not in DOM:', this.options.baseDiv); + } + } + this.renderer.postUpdate(cause); + console.timeEnd(`Viewer update (vu) (${counter})`); + } +} diff --git a/packages/editor/src/performance/performance-module.ts b/packages/editor/src/performance/performance-module.ts new file mode 100644 index 00000000..745bc57c --- /dev/null +++ b/packages/editor/src/performance/performance-module.ts @@ -0,0 +1,22 @@ +import { DiagramLoader, FeatureModule, GLSPActionDispatcher, GLSPModelSource, ModelViewer, TYPES } from '@eclipse-glsp/client'; +import { PerfActionDispatcher } from './perf-action-dispatcher'; +import { PerfCommandStack } from './perf-command-stack'; +import { PerfDiagramLoader } from './perf-diagram-loader'; +import { PerfModelViewer } from './perf-viewer'; +import { PerfGLSPModelSource } from './perf-model-source'; + +export function createPerformanceModule(enabled?: boolean): FeatureModule { + return new FeatureModule( + (bind, unbind, isBound, rebind) => { + if (!enabled) { + return; + } + rebind(DiagramLoader).to(PerfDiagramLoader).inSingletonScope(); + rebind(GLSPActionDispatcher).to(PerfActionDispatcher).inSingletonScope(); + rebind(ModelViewer).to(PerfModelViewer).inSingletonScope(); + rebind(TYPES.ICommandStack).to(PerfCommandStack).inSingletonScope(); + rebind(GLSPModelSource).to(PerfGLSPModelSource).inSingletonScope(); + }, + { featureId: Symbol('performance') } + ); +}