UBERF-8098: Basic client metrics in UI (#6556)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-09-13 22:58:11 +07:00 committed by GitHub
parent 262f1e2634
commit c9a327ae63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 125 additions and 92 deletions

View File

@ -344,7 +344,7 @@ async function tryLoadModel (
reload: boolean, reload: boolean,
persistence?: TxPersistenceStore persistence?: TxPersistenceStore
): Promise<LoadModelResponse> { ): Promise<LoadModelResponse> {
const current = (await ctx.with('persistence-load', {}, async () => await persistence?.load())) ?? { const current = (await ctx.with('persistence-load', {}, () => persistence?.load())) ?? {
full: true, full: true,
transactions: [], transactions: [],
hash: '' hash: ''
@ -365,14 +365,11 @@ async function tryLoadModel (
} }
// Save concatenated // Save concatenated
void (await ctx.with( void (await ctx.with('persistence-store', {}, (ctx) =>
'persistence-store', persistence?.store({
{}, ...result,
async (ctx) => transactions: !result.full ? current.transactions.concat(result.transactions) : result.transactions
await persistence?.store({ })
...result,
transactions: !result.full ? current.transactions.concat(result.transactions) : result.transactions
})
)) ))
if (!result.full && !reload) { if (!result.full && !reload) {
@ -405,10 +402,8 @@ async function loadModel (
): Promise<LoadModelResponse> { ): Promise<LoadModelResponse> {
const t = Date.now() const t = Date.now()
const modelResponse = await ctx.with( const modelResponse = await ctx.with('try-load-model', { reload }, (ctx) =>
'try-load-model', tryLoadModel(ctx, conn, reload, persistence)
{ reload },
async (ctx) => await tryLoadModel(ctx, conn, reload, persistence)
) )
if (reload && modelResponse.full) { if (reload && modelResponse.full) {
@ -423,9 +418,7 @@ async function loadModel (
) )
} }
await ctx.with('build-model', {}, async (ctx) => { await ctx.with('build-model', {}, (ctx) => buildModel(ctx, modelResponse, allowedPlugins, configs, hierarchy, model))
await buildModel(ctx, modelResponse, allowedPlugins, configs, hierarchy, model)
})
return modelResponse return modelResponse
} }

View File

@ -16,6 +16,7 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import core, { import core, {
MeasureMetricsContext,
TxOperations, TxOperations,
TxProcessor, TxProcessor,
getCurrentAccount, getCurrentAccount,
@ -90,6 +91,8 @@ export interface OptimisticTxes {
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>> pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
} }
export const uiContext = new MeasureMetricsContext('client-ui', {})
class UIClient extends TxOperations implements Client, OptimisticTxes { class UIClient extends TxOperations implements Client, OptimisticTxes {
hook = getMetadata(plugin.metadata.ClientHook) hook = getMetadata(plugin.metadata.ClientHook)
constructor ( constructor (

View File

@ -28,6 +28,7 @@ import core, {
FindOptions, FindOptions,
FindResult, FindResult,
LoadModelResponse, LoadModelResponse,
MeasureMetricsContext,
Ref, Ref,
SearchOptions, SearchOptions,
SearchQuery, SearchQuery,
@ -38,7 +39,8 @@ import core, {
TxHandler, TxHandler,
TxResult, TxResult,
generateId, generateId,
toFindResult toFindResult,
type MeasureContext
} from '@hcengineering/core' } from '@hcengineering/core'
import { PlatformError, UNAUTHORIZED, broadcastEvent, getMetadata, unknownError } from '@hcengineering/platform' import { PlatformError, UNAUTHORIZED, broadcastEvent, getMetadata, unknownError } from '@hcengineering/platform'
@ -96,6 +98,7 @@ class Connection implements ClientConnection {
rpcHandler = new RPCHandler() rpcHandler = new RPCHandler()
constructor ( constructor (
private readonly ctx: MeasureContext,
private readonly url: string, private readonly url: string,
private readonly handler: TxHandler, private readonly handler: TxHandler,
readonly workspace: string, readonly workspace: string,
@ -121,7 +124,7 @@ class Connection implements ClientConnection {
this.sessionId = generateId() this.sessionId = generateId()
} }
this.scheduleOpen(false) this.scheduleOpen(this.ctx, false)
} }
private schedulePing (socketId: number): void { private schedulePing (socketId: number): void {
@ -181,26 +184,33 @@ class Connection implements ClientConnection {
delay = 0 delay = 0
onConnectHandlers: (() => void)[] = [] onConnectHandlers: (() => void)[] = []
private waitOpenConnection (): Promise<void> | undefined { private waitOpenConnection (ctx: MeasureContext): Promise<void> | undefined {
if (this.isConnected()) { if (this.isConnected()) {
return undefined return undefined
} }
return new Promise((resolve) => { return ctx.with(
this.onConnectHandlers.push(() => { 'wait-connection',
resolve() {},
}) (ctx) =>
// Websocket is null for first time new Promise((resolve) => {
this.scheduleOpen(false) 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 (force) {
if (this.websocket !== null) { ctx.withSync('close-ws', {}, () => {
this.websocket.close() if (this.websocket !== null) {
this.websocket = null this.websocket.close()
} this.websocket = null
}
})
clearTimeout(this.openAction) clearTimeout(this.openAction)
this.openAction = undefined this.openAction = undefined
} }
@ -210,11 +220,11 @@ class Connection implements ClientConnection {
const socketId = ++this.sockets const socketId = ++this.sockets
// Re create socket in case of error, if not closed // Re create socket in case of error, if not closed
if (this.delay === 0) { if (this.delay === 0) {
this.openConnection(socketId) this.openConnection(ctx, socketId)
} else { } else {
this.openAction = setTimeout(() => { this.openAction = setTimeout(() => {
this.openAction = undefined this.openAction = undefined
this.openConnection(socketId) this.openConnection(ctx, socketId)
}, this.delay * 1000) }, 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 this.binaryMode = false
// Use defined factory or browser default one. // Use defined factory or browser default one.
const clientSocketFactory = const clientSocketFactory =
@ -391,7 +401,9 @@ class Connection implements ClientConnection {
if (socketId !== this.sockets) { if (socketId !== this.sockets) {
return 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) { if (socketId !== this.sockets) {
wsocket.close() wsocket.close()
@ -405,7 +417,7 @@ class Connection implements ClientConnection {
this.dialTimer = null this.dialTimer = null
if (!opened && !this.closed) { if (!opened && !this.closed) {
void this.opt?.onDialTimeout?.() void this.opt?.onDialTimeout?.()
this.scheduleOpen(true) this.scheduleOpen(this.ctx, true)
} }
}, dialTimeout) }, dialTimeout)
} }
@ -434,7 +446,7 @@ class Connection implements ClientConnection {
} }
// console.log('client websocket closed', socketId, ev?.reason) // console.log('client websocket closed', socketId, ev?.reason)
void broadcastEvent(client.event.NetworkRequests, -1) void broadcastEvent(client.event.NetworkRequests, -1)
this.scheduleOpen(true) this.scheduleOpen(this.ctx, true)
} }
wsocket.onopen = () => { wsocket.onopen = () => {
if (this.websocket !== wsocket) { if (this.websocket !== wsocket) {
@ -450,7 +462,7 @@ class Connection implements ClientConnection {
binary: useBinary, binary: useBinary,
compression: useCompression 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) => { wsocket.onerror = (event: any) => {
@ -477,60 +489,63 @@ class Connection implements ClientConnection {
measure?: (time: number, result: any, serverTime: number, queue: number, toRecieve: number) => void measure?: (time: number, result: any, serverTime: number, queue: number, toRecieve: number) => void
allowReconnect?: boolean allowReconnect?: boolean
}): Promise<any> { }): Promise<any> {
if (this.closed) { return await this.ctx.newChild('send-request', {}).with(data.method, {}, async (ctx) => {
throw new PlatformError(unknownError('connection closed')) if (this.closed) {
} throw new PlatformError(unknownError('connection closed'))
}
if (data.once === true) { if (data.once === true) {
// Check if has same request already then skip // Check if has same request already then skip
const dparams = JSON.stringify(data.params) const dparams = JSON.stringify(data.params)
for (const [, v] of this.requests) { for (const [, v] of this.requests) {
if (v.method === data.method && JSON.stringify(v.params) === dparams) { if (v.method === data.method && JSON.stringify(v.params) === dparams) {
// We have same unanswered, do not add one more. // We have same unanswered, do not add one more.
return return
}
} }
} }
}
const id = this.lastId++ const id = this.lastId++
const promise = new RequestPromise(data.method, data.params, data.handleResult) const promise = new RequestPromise(data.method, data.params, data.handleResult)
promise.handleTime = data.measure promise.handleTime = data.measure
const w = this.waitOpenConnection() const w = this.waitOpenConnection(ctx)
if (w instanceof Promise) { if (w instanceof Promise) {
await w await w
} }
this.requests.set(id, promise) this.requests.set(id, promise)
const sendData = async (): Promise<void> => { const sendData = async (): Promise<void> => {
if (this.websocket?.readyState === ClientSocketReadyState.OPEN) { if (this.websocket?.readyState === ClientSocketReadyState.OPEN) {
promise.startTime = Date.now() promise.startTime = Date.now()
this.websocket?.send( const dta = ctx.withSync('serialize', {}, () =>
this.rpcHandler.serialize( this.rpcHandler.serialize(
{ {
method: data.method, method: data.method,
params: data.params, params: data.params,
id, id,
time: Date.now() time: Date.now()
}, },
this.binaryMode this.binaryMode
)
) )
) ctx.withSync('send-data', {}, () => this.websocket?.send(dta))
}
} }
} if (data.allowReconnect ?? true) {
if (data.allowReconnect ?? true) { promise.reconnect = () => {
promise.reconnect = () => { setTimeout(async () => {
setTimeout(async () => { // In case we don't have response yet.
// In case we don't have response yet. if (this.requests.has(id) && ((await data.retry?.()) ?? true)) {
if (this.requests.has(id) && ((await data.retry?.()) ?? true)) { void sendData()
void sendData() }
} }, 50)
}, 50) }
} }
} void ctx.with('send-data', {}, () => sendData())
void sendData() void ctx.with('broadcast-event', {}, () => broadcastEvent(client.event.NetworkRequests, this.requests.size))
void broadcastEvent(client.event.NetworkRequests, this.requests.size) return await promise.promise
return await promise.promise })
} }
async loadModel (last: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> { async loadModel (last: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> {
@ -652,5 +667,12 @@ export function connect (
user: string, user: string,
opt?: ClientFactoryOptions opt?: ClientFactoryOptions
): ClientConnection { ): ClientConnection {
return new Connection(url, handler, workspace, user, opt) return new Connection(
opt?.ctx?.newChild?.('connection', {}) ?? new MeasureMetricsContext('connection', {}),
url,
handler,
workspace,
user,
opt
)
} }

View File

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import core, { RateLimiter, concatLink } from '@hcengineering/core' import core, { RateLimiter, concatLink, metricsAggregate, type Metrics } from '@hcengineering/core'
import login from '@hcengineering/login' import login from '@hcengineering/login'
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform' import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
import presentation, { getClient, isAdminUser } from '@hcengineering/presentation' import presentation, { getClient, isAdminUser, uiContext } from '@hcengineering/presentation'
import { Button, IconArrowLeft, IconArrowRight, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui' import { Button, IconArrowLeft, IconArrowRight, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui'
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte' import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import MetricsInfo from './statistics/MetricsInfo.svelte'
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? '' const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
const token: string = getMetadata(presentation.metadata.Token) ?? '' const token: string = getMetadata(presentation.metadata.Token) ?? ''
@ -133,6 +134,14 @@
document.body.removeChild(link) document.body.removeChild(link)
fetchStats(0) fetchStats(0)
} }
let metrics: Metrics | undefined
function update (tick: number) {
metrics = metricsAggregate(uiContext.metrics)
}
$: update($ticker)
</script> </script>
{#if isAdminUser()} {#if isAdminUser()}
@ -231,6 +240,10 @@
</div> </div>
{/if} {/if}
{#if metrics}
<MetricsInfo {metrics} />
{/if}
<style lang="scss"> <style lang="scss">
.greyed { .greyed {
color: rgba(black, 0.5); color: rgba(black, 0.5);

View File

@ -4,14 +4,15 @@ import core, {
ClientConnectEvent, ClientConnectEvent,
concatLink, concatLink,
getCurrentAccount, getCurrentAccount,
MeasureMetricsContext, isWorkspaceCreating,
metricsToString, metricsToString,
setCurrentAccount, setCurrentAccount,
versionToString, versionToString,
isWorkspaceCreating,
type Account, type Account,
type AccountClient, type AccountClient,
type Client, type Client,
type MeasureContext,
type MeasureMetricsContext,
type Version type Version
} from '@hcengineering/core' } from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login' import login, { loginId } from '@hcengineering/login'
@ -22,7 +23,8 @@ import presentation, {
purgeClient, purgeClient,
refreshClient, refreshClient,
setClient, setClient,
setPresentationCookie setPresentationCookie,
uiContext
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import { import {
fetchMetadataLocalStorage, fetchMetadataLocalStorage,
@ -51,7 +53,7 @@ export async function disconnect (): Promise<void> {
} }
export async function connect (title: string): Promise<Client | undefined> { export async function connect (title: string): Promise<Client | undefined> {
const ctx = new MeasureMetricsContext('connect', {}) const ctx = uiContext.newChild('connect', {})
const loc = getCurrentLocation() const loc = getCurrentLocation()
const ws = loc.path[1] const ws = loc.path[1]
if (ws === undefined) { if (ws === undefined) {
@ -315,12 +317,12 @@ export async function connect (title: string): Promise<Client | undefined> {
await ctx.with('broadcast-connected', {}, async () => { await ctx.with('broadcast-connected', {}, async () => {
await broadcastEvent(plugin.event.NotifyConnection, getCurrentAccount()) await broadcastEvent(plugin.event.NotifyConnection, getCurrentAccount())
}) })
console.log(metricsToString(ctx.metrics, 'connect', 50)) console.log(metricsToString((ctx as MeasureMetricsContext).metrics, 'connect', 50))
return newClient return newClient
} }
async function createEmployee ( async function createEmployee (
ctx: MeasureMetricsContext, ctx: MeasureContext,
ws: string, ws: string,
me: Account, me: Account,
newClient: AccountClient newClient: AccountClient