Skip to content

Commit

Permalink
UBERF-8098: Basic client metrics in UI (#6556)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Sobolev <[email protected]>
  • Loading branch information
haiodo committed Sep 13, 2024
1 parent 262f1e2 commit c9a327a
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 93 deletions.
25 changes: 9 additions & 16 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ async function tryLoadModel (
reload: boolean,
persistence?: TxPersistenceStore
): Promise<LoadModelResponse> {
const current = (await ctx.with('persistence-load', {}, async () => await persistence?.load())) ?? {
const current = (await ctx.with('persistence-load', {}, () => persistence?.load())) ?? {
full: true,
transactions: [],
hash: ''
Expand All @@ -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) {
Expand Down Expand Up @@ -405,10 +402,8 @@ async function loadModel (
): Promise<LoadModelResponse> {
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) {
Expand All @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions packages/presentation/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { Analytics } from '@hcengineering/analytics'
import core, {
MeasureMetricsContext,
TxOperations,
TxProcessor,
getCurrentAccount,
Expand Down Expand Up @@ -90,6 +91,8 @@ export interface OptimisticTxes {
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
}

export const uiContext = new MeasureMetricsContext('client-ui', {})

class UIClient extends TxOperations implements Client, OptimisticTxes {
hook = getMetadata(plugin.metadata.ClientHook)
constructor (
Expand Down
160 changes: 91 additions & 69 deletions plugins/client-resources/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import core, {
FindOptions,
FindResult,
LoadModelResponse,
MeasureMetricsContext,
Ref,
SearchOptions,
SearchQuery,
Expand All @@ -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'

Expand Down Expand Up @@ -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,
Expand All @@ -121,7 +124,7 @@ class Connection implements ClientConnection {
this.sessionId = generateId()
}

this.scheduleOpen(false)
this.scheduleOpen(this.ctx, false)
}

private schedulePing (socketId: number): void {
Expand Down Expand Up @@ -181,26 +184,33 @@ class Connection implements ClientConnection {
delay = 0
onConnectHandlers: (() => void)[] = []

private waitOpenConnection (): Promise<void> | undefined {
private waitOpenConnection (ctx: MeasureContext): Promise<void> | 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
}
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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 =
Expand All @@ -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()
Expand All @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) => {
Expand All @@ -477,60 +489,63 @@ class Connection implements ClientConnection {
measure?: (time: number, result: any, serverTime: number, queue: number, toRecieve: number) => void
allowReconnect?: boolean
}): Promise<any> {
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<void> => {
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<void> => {
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<Tx[] | LoadModelResponse> {
Expand Down Expand Up @@ -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
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<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 { 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 EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import MetricsInfo from './statistics/MetricsInfo.svelte'
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
const token: string = getMetadata(presentation.metadata.Token) ?? ''
Expand Down Expand Up @@ -133,6 +134,14 @@
document.body.removeChild(link)
fetchStats(0)
}
let metrics: Metrics | undefined
function update (tick: number) {
metrics = metricsAggregate(uiContext.metrics)
}
$: update($ticker)
</script>

{#if isAdminUser()}
Expand Down Expand Up @@ -231,6 +240,10 @@
</div>
{/if}

{#if metrics}
<MetricsInfo {metrics} />
{/if}

<style lang="scss">
.greyed {
color: rgba(black, 0.5);
Expand Down
Loading

0 comments on commit c9a327a

Please sign in to comment.