From c9a327ae6300f301082a53f9031a7352c7f1ee75 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Fri, 13 Sep 2024 22:58:11 +0700 Subject: [PATCH] UBERF-8098: Basic client metrics in UI (#6556) Signed-off-by: Andrey Sobolev --- packages/core/src/client.ts | 25 +-- packages/presentation/src/utils.ts | 3 + plugins/client-resources/src/connection.ts | 160 ++++++++++-------- .../components/ServerManagerGeneral.svelte | 17 +- plugins/workbench-resources/src/connect.ts | 14 +- 5 files changed, 126 insertions(+), 93 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 5d4d23580d..2b063ea834 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -344,7 +344,7 @@ async function tryLoadModel ( reload: boolean, persistence?: TxPersistenceStore ): Promise { - const current = (await ctx.with('persistence-load', {}, async () => await persistence?.load())) ?? { + const current = (await ctx.with('persistence-load', {}, () => persistence?.load())) ?? { full: true, transactions: [], hash: '' @@ -365,14 +365,11 @@ async function tryLoadModel ( } // Save concatenated - void (await ctx.with( - 'persistence-store', - {}, - async (ctx) => - await persistence?.store({ - ...result, - transactions: !result.full ? current.transactions.concat(result.transactions) : result.transactions - }) + void (await ctx.with('persistence-store', {}, (ctx) => + persistence?.store({ + ...result, + transactions: !result.full ? current.transactions.concat(result.transactions) : result.transactions + }) )) if (!result.full && !reload) { @@ -405,10 +402,8 @@ async function loadModel ( ): Promise { const t = Date.now() - const modelResponse = await ctx.with( - 'try-load-model', - { reload }, - async (ctx) => await tryLoadModel(ctx, conn, reload, persistence) + const modelResponse = await ctx.with('try-load-model', { reload }, (ctx) => + tryLoadModel(ctx, conn, reload, persistence) ) if (reload && modelResponse.full) { @@ -423,9 +418,7 @@ async function loadModel ( ) } - await ctx.with('build-model', {}, async (ctx) => { - await buildModel(ctx, modelResponse, allowedPlugins, configs, hierarchy, model) - }) + await ctx.with('build-model', {}, (ctx) => buildModel(ctx, modelResponse, allowedPlugins, configs, hierarchy, model)) return modelResponse } diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 5e199f223c..34e9900b9e 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -16,6 +16,7 @@ import { Analytics } from '@hcengineering/analytics' import core, { + MeasureMetricsContext, TxOperations, TxProcessor, getCurrentAccount, @@ -90,6 +91,8 @@ export interface OptimisticTxes { pendingCreatedDocs: Writable, boolean>> } +export const uiContext = new MeasureMetricsContext('client-ui', {}) + class UIClient extends TxOperations implements Client, OptimisticTxes { hook = getMetadata(plugin.metadata.ClientHook) constructor ( diff --git a/plugins/client-resources/src/connection.ts b/plugins/client-resources/src/connection.ts index a6e3180cdd..13d81464f2 100644 --- a/plugins/client-resources/src/connection.ts +++ b/plugins/client-resources/src/connection.ts @@ -28,6 +28,7 @@ import core, { FindOptions, FindResult, LoadModelResponse, + MeasureMetricsContext, Ref, SearchOptions, SearchQuery, @@ -38,7 +39,8 @@ import core, { TxHandler, TxResult, generateId, - toFindResult + toFindResult, + type MeasureContext } from '@hcengineering/core' import { PlatformError, UNAUTHORIZED, broadcastEvent, getMetadata, unknownError } from '@hcengineering/platform' @@ -96,6 +98,7 @@ class Connection implements ClientConnection { rpcHandler = new RPCHandler() constructor ( + private readonly ctx: MeasureContext, private readonly url: string, private readonly handler: TxHandler, readonly workspace: string, @@ -121,7 +124,7 @@ class Connection implements ClientConnection { this.sessionId = generateId() } - this.scheduleOpen(false) + this.scheduleOpen(this.ctx, false) } private schedulePing (socketId: number): void { @@ -181,26 +184,33 @@ class Connection implements ClientConnection { delay = 0 onConnectHandlers: (() => void)[] = [] - private waitOpenConnection (): Promise | undefined { + private waitOpenConnection (ctx: MeasureContext): Promise | undefined { if (this.isConnected()) { return undefined } - return new Promise((resolve) => { - this.onConnectHandlers.push(() => { - resolve() - }) - // Websocket is null for first time - this.scheduleOpen(false) - }) + return ctx.with( + 'wait-connection', + {}, + (ctx) => + new Promise((resolve) => { + this.onConnectHandlers.push(() => { + resolve() + }) + // Websocket is null for first time + this.scheduleOpen(ctx, false) + }) + ) } - scheduleOpen (force: boolean): void { + scheduleOpen (ctx: MeasureContext, force: boolean): void { if (force) { - if (this.websocket !== null) { - this.websocket.close() - this.websocket = null - } + ctx.withSync('close-ws', {}, () => { + if (this.websocket !== null) { + this.websocket.close() + this.websocket = null + } + }) clearTimeout(this.openAction) this.openAction = undefined } @@ -210,11 +220,11 @@ class Connection implements ClientConnection { const socketId = ++this.sockets // Re create socket in case of error, if not closed if (this.delay === 0) { - this.openConnection(socketId) + this.openConnection(ctx, socketId) } else { this.openAction = setTimeout(() => { this.openAction = undefined - this.openConnection(socketId) + this.openConnection(ctx, socketId) }, this.delay * 1000) } } @@ -377,7 +387,7 @@ class Connection implements ClientConnection { } } - private openConnection (socketId: number): void { + private openConnection (ctx: MeasureContext, socketId: number): void { this.binaryMode = false // Use defined factory or browser default one. const clientSocketFactory = @@ -391,7 +401,9 @@ class Connection implements ClientConnection { if (socketId !== this.sockets) { return } - const wsocket = clientSocketFactory(this.url + `?sessionId=${this.sessionId}`) + const wsocket = ctx.withSync('create-socket', {}, () => + clientSocketFactory(this.url + `?sessionId=${this.sessionId}`) + ) if (socketId !== this.sockets) { wsocket.close() @@ -405,7 +417,7 @@ class Connection implements ClientConnection { this.dialTimer = null if (!opened && !this.closed) { void this.opt?.onDialTimeout?.() - this.scheduleOpen(true) + this.scheduleOpen(this.ctx, true) } }, dialTimeout) } @@ -434,7 +446,7 @@ class Connection implements ClientConnection { } // console.log('client websocket closed', socketId, ev?.reason) void broadcastEvent(client.event.NetworkRequests, -1) - this.scheduleOpen(true) + this.scheduleOpen(this.ctx, true) } wsocket.onopen = () => { if (this.websocket !== wsocket) { @@ -450,7 +462,7 @@ class Connection implements ClientConnection { binary: useBinary, compression: useCompression } - this.websocket?.send(this.rpcHandler.serialize(helloRequest, false)) + ctx.withSync('send-hello', {}, () => this.websocket?.send(this.rpcHandler.serialize(helloRequest, false))) } wsocket.onerror = (event: any) => { @@ -477,60 +489,63 @@ class Connection implements ClientConnection { measure?: (time: number, result: any, serverTime: number, queue: number, toRecieve: number) => void allowReconnect?: boolean }): Promise { - if (this.closed) { - throw new PlatformError(unknownError('connection closed')) - } + return await this.ctx.newChild('send-request', {}).with(data.method, {}, async (ctx) => { + if (this.closed) { + throw new PlatformError(unknownError('connection closed')) + } - if (data.once === true) { - // Check if has same request already then skip - const dparams = JSON.stringify(data.params) - for (const [, v] of this.requests) { - if (v.method === data.method && JSON.stringify(v.params) === dparams) { - // We have same unanswered, do not add one more. - return + if (data.once === true) { + // Check if has same request already then skip + const dparams = JSON.stringify(data.params) + for (const [, v] of this.requests) { + if (v.method === data.method && JSON.stringify(v.params) === dparams) { + // We have same unanswered, do not add one more. + return + } } } - } - const id = this.lastId++ - const promise = new RequestPromise(data.method, data.params, data.handleResult) - promise.handleTime = data.measure + const id = this.lastId++ + const promise = new RequestPromise(data.method, data.params, data.handleResult) + promise.handleTime = data.measure - const w = this.waitOpenConnection() - if (w instanceof Promise) { - await w - } - this.requests.set(id, promise) - const sendData = async (): Promise => { - if (this.websocket?.readyState === ClientSocketReadyState.OPEN) { - promise.startTime = Date.now() - - this.websocket?.send( - this.rpcHandler.serialize( - { - method: data.method, - params: data.params, - id, - time: Date.now() - }, - this.binaryMode + const w = this.waitOpenConnection(ctx) + if (w instanceof Promise) { + await w + } + this.requests.set(id, promise) + const sendData = async (): Promise => { + if (this.websocket?.readyState === ClientSocketReadyState.OPEN) { + promise.startTime = Date.now() + + const dta = ctx.withSync('serialize', {}, () => + this.rpcHandler.serialize( + { + method: data.method, + params: data.params, + id, + time: Date.now() + }, + this.binaryMode + ) ) - ) + ctx.withSync('send-data', {}, () => this.websocket?.send(dta)) + } } - } - if (data.allowReconnect ?? true) { - promise.reconnect = () => { - setTimeout(async () => { - // In case we don't have response yet. - if (this.requests.has(id) && ((await data.retry?.()) ?? true)) { - void sendData() - } - }, 50) + if (data.allowReconnect ?? true) { + promise.reconnect = () => { + setTimeout(async () => { + // In case we don't have response yet. + if (this.requests.has(id) && ((await data.retry?.()) ?? true)) { + void sendData() + } + }, 50) + } } - } - void sendData() - void broadcastEvent(client.event.NetworkRequests, this.requests.size) - return await promise.promise + void ctx.with('send-data', {}, () => sendData()) + void ctx.with('broadcast-event', {}, () => broadcastEvent(client.event.NetworkRequests, this.requests.size)) + return await promise.promise + }) } async loadModel (last: Timestamp, hash?: string): Promise { @@ -652,5 +667,12 @@ export function connect ( user: string, opt?: ClientFactoryOptions ): ClientConnection { - return new Connection(url, handler, workspace, user, opt) + return new Connection( + opt?.ctx?.newChild?.('connection', {}) ?? new MeasureMetricsContext('connection', {}), + url, + handler, + workspace, + user, + opt + ) } diff --git a/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte b/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte index f0926d679d..c387481b0b 100644 --- a/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte +++ b/plugins/workbench-resources/src/components/ServerManagerGeneral.svelte @@ -1,10 +1,11 @@ {#if isAdminUser()} @@ -231,6 +240,10 @@ {/if} +{#if metrics} + +{/if} +