mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-05 07:14:22 +00:00
UBERF-8098: Basic client metrics in UI (#6556)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
262f1e2634
commit
c9a327ae63
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user