mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 13:17:45 +02:00
UBERF-4319: Performance changes (#4474)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -38,7 +38,8 @@ import core, {
|
||||
generateId,
|
||||
SearchQuery,
|
||||
SearchOptions,
|
||||
SearchResult
|
||||
SearchResult,
|
||||
MeasureDoneOperation
|
||||
} from '@hcengineering/core'
|
||||
import { PlatformError, UNAUTHORIZED, broadcastEvent, getMetadata, unknownError } from '@hcengineering/platform'
|
||||
|
||||
@@ -376,6 +377,27 @@ class Connection implements ClientConnection {
|
||||
return await promise.promise
|
||||
}
|
||||
|
||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
||||
const dateNow = Date.now()
|
||||
|
||||
// Send measure-start
|
||||
const mid = await this.sendRequest({
|
||||
method: 'measure',
|
||||
params: [operationName]
|
||||
})
|
||||
return async () => {
|
||||
const serverTime: number = await this.sendRequest({
|
||||
method: 'measure-done',
|
||||
params: [operationName, mid]
|
||||
})
|
||||
|
||||
return {
|
||||
time: Date.now() - dateNow,
|
||||
serverTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadModel (last: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> {
|
||||
return await this.sendRequest({ method: 'loadModel', params: [last, hash] })
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
import type { AccountClient, ClientConnectEvent } from '@hcengineering/core'
|
||||
import type { Plugin, Resource } from '@hcengineering/platform'
|
||||
import { Metadata, plugin } from '@hcengineering/platform'
|
||||
// import type { LiveQuery } from '@hcengineering/query'
|
||||
|
||||
// export type Connection = Client & LiveQuery & TxOperations
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
||||
@@ -30,7 +30,8 @@ import core, {
|
||||
type WithLookup,
|
||||
type SearchQuery,
|
||||
type SearchOptions,
|
||||
type SearchResult
|
||||
type SearchResult,
|
||||
type MeasureDoneOperation
|
||||
} from '@hcengineering/core'
|
||||
import { devModelId } from '@hcengineering/devmodel'
|
||||
import { Builder } from '@hcengineering/model'
|
||||
@@ -68,6 +69,10 @@ class ModelClient implements AccountClient {
|
||||
}
|
||||
}
|
||||
|
||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
||||
return await this.client.measure(operationName)
|
||||
}
|
||||
|
||||
notify?: (tx: Tx) => void
|
||||
|
||||
getHierarchy (): Hierarchy {
|
||||
|
||||
@@ -352,97 +352,105 @@
|
||||
return
|
||||
}
|
||||
|
||||
const operations = client.apply(_id)
|
||||
// TODO: We need a measure client and mark all operations with it as measure under one root,
|
||||
// to prevent other operations to infer our measurement.
|
||||
const doneOp = await getClient().measure('tracker.createIssue')
|
||||
|
||||
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
|
||||
const incResult = await client.updateDoc(
|
||||
tracker.class.Project,
|
||||
core.space.Space,
|
||||
_space,
|
||||
{
|
||||
$inc: { sequence: 1 }
|
||||
},
|
||||
true
|
||||
)
|
||||
try {
|
||||
const operations = client.apply(_id)
|
||||
|
||||
const value: DocData<Issue> = {
|
||||
title: getTitle(object.title),
|
||||
description: object.description,
|
||||
assignee: object.assignee,
|
||||
component: object.component,
|
||||
milestone: object.milestone,
|
||||
number: (incResult as any).object.sequence,
|
||||
status: object.status,
|
||||
priority: object.priority,
|
||||
rank: calcRank(lastOne, undefined),
|
||||
comments: 0,
|
||||
subIssues: 0,
|
||||
dueDate: object.dueDate,
|
||||
parents:
|
||||
parentIssue != null
|
||||
? [
|
||||
{ parentId: parentIssue._id, parentTitle: parentIssue.title, space: parentIssue.space },
|
||||
...parentIssue.parents
|
||||
]
|
||||
: [],
|
||||
reportedTime: 0,
|
||||
remainingTime: 0,
|
||||
estimation: object.estimation,
|
||||
reports: 0,
|
||||
relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : [],
|
||||
childInfo: [],
|
||||
kind
|
||||
}
|
||||
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
|
||||
const incResult = await client.updateDoc(
|
||||
tracker.class.Project,
|
||||
core.space.Space,
|
||||
_space,
|
||||
{
|
||||
$inc: { sequence: 1 }
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
await docCreateManager.commit(operations, _id, _space, value)
|
||||
const value: DocData<Issue> = {
|
||||
title: getTitle(object.title),
|
||||
description: object.description,
|
||||
assignee: object.assignee,
|
||||
component: object.component,
|
||||
milestone: object.milestone,
|
||||
number: (incResult as any).object.sequence,
|
||||
status: object.status,
|
||||
priority: object.priority,
|
||||
rank: calcRank(lastOne, undefined),
|
||||
comments: 0,
|
||||
subIssues: 0,
|
||||
dueDate: object.dueDate,
|
||||
parents:
|
||||
parentIssue != null
|
||||
? [
|
||||
{ parentId: parentIssue._id, parentTitle: parentIssue.title, space: parentIssue.space },
|
||||
...parentIssue.parents
|
||||
]
|
||||
: [],
|
||||
reportedTime: 0,
|
||||
remainingTime: 0,
|
||||
estimation: object.estimation,
|
||||
reports: 0,
|
||||
relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : [],
|
||||
childInfo: [],
|
||||
kind
|
||||
}
|
||||
|
||||
await operations.addCollection(
|
||||
tracker.class.Issue,
|
||||
_space,
|
||||
parentIssue?._id ?? tracker.ids.NoParent,
|
||||
parentIssue?._class ?? tracker.class.Issue,
|
||||
'subIssues',
|
||||
value,
|
||||
_id
|
||||
)
|
||||
for (const label of object.labels) {
|
||||
await operations.addCollection(label._class, label.space, _id, tracker.class.Issue, 'labels', {
|
||||
title: label.title,
|
||||
color: label.color,
|
||||
tag: label.tag
|
||||
})
|
||||
}
|
||||
await docCreateManager.commit(operations, _id, _space, value)
|
||||
|
||||
if (relatedTo !== undefined) {
|
||||
const doc = await client.findOne(tracker.class.Issue, { _id })
|
||||
if (doc !== undefined) {
|
||||
if (client.getHierarchy().isDerived(relatedTo._class, tracker.class.Issue)) {
|
||||
await updateIssueRelation(operations, relatedTo as Issue, doc, 'relations', '$push')
|
||||
} else {
|
||||
const update = await getResource(chunter.backreference.Update)
|
||||
await update(doc, 'relations', [relatedTo], tracker.string.AddedReference)
|
||||
await operations.addCollection(
|
||||
tracker.class.Issue,
|
||||
_space,
|
||||
parentIssue?._id ?? tracker.ids.NoParent,
|
||||
parentIssue?._class ?? tracker.class.Issue,
|
||||
'subIssues',
|
||||
value,
|
||||
_id
|
||||
)
|
||||
for (const label of object.labels) {
|
||||
await operations.addCollection(label._class, label.space, _id, tracker.class.Issue, 'labels', {
|
||||
title: label.title,
|
||||
color: label.color,
|
||||
tag: label.tag
|
||||
})
|
||||
}
|
||||
|
||||
if (relatedTo !== undefined) {
|
||||
const doc = await client.findOne(tracker.class.Issue, { _id })
|
||||
if (doc !== undefined) {
|
||||
if (client.getHierarchy().isDerived(relatedTo._class, tracker.class.Issue)) {
|
||||
await updateIssueRelation(operations, relatedTo as Issue, doc, 'relations', '$push')
|
||||
} else {
|
||||
const update = await getResource(chunter.backreference.Update)
|
||||
await update(doc, 'relations', [relatedTo], tracker.string.AddedReference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await operations.commit()
|
||||
await descriptionBox.createAttachments(_id)
|
||||
addNotification(
|
||||
await translate(tracker.string.IssueCreated, {}, $themeStore.language),
|
||||
getTitle(object.title),
|
||||
IssueNotification,
|
||||
{
|
||||
issueId: _id,
|
||||
subTitlePostfix: (await translate(tracker.string.CreatedOne, {}, $themeStore.language)).toLowerCase(),
|
||||
issueUrl: currentProject != null && generateIssueShortLink(getIssueId(currentProject, value as Issue))
|
||||
}
|
||||
)
|
||||
console.log('createIssue measure', await doneOp())
|
||||
|
||||
draftController.remove()
|
||||
descriptionBox?.removeDraft(false)
|
||||
isAssigneeTouched = false
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
await doneOp() // Complete in case of error
|
||||
}
|
||||
|
||||
await operations.commit()
|
||||
|
||||
await descriptionBox.createAttachments(_id)
|
||||
|
||||
addNotification(
|
||||
await translate(tracker.string.IssueCreated, {}, $themeStore.language),
|
||||
getTitle(object.title),
|
||||
IssueNotification,
|
||||
{
|
||||
issueId: _id,
|
||||
subTitlePostfix: (await translate(tracker.string.CreatedOne, {}, $themeStore.language)).toLowerCase(),
|
||||
issueUrl: currentProject != null && generateIssueShortLink(getIssueId(currentProject, value as Issue))
|
||||
}
|
||||
)
|
||||
|
||||
draftController.remove()
|
||||
descriptionBox?.removeDraft(false)
|
||||
isAssigneeTouched = false
|
||||
}
|
||||
|
||||
async function setParentIssue (): Promise<void> {
|
||||
|
||||
@@ -157,7 +157,9 @@
|
||||
|
||||
async function handleSelection (evt: Event, selection: number): Promise<void> {
|
||||
const item = items[selection]
|
||||
|
||||
if (item == null) {
|
||||
return
|
||||
}
|
||||
if (item.item !== undefined) {
|
||||
const doc = item.item.doc
|
||||
void client.findOne(doc._class, { _id: doc._id }).then((value) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
import { metricsToRows } from '@hcengineering/core'
|
||||
import { Metrics } from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
|
||||
import presentation, { createQuery } from '@hcengineering/presentation'
|
||||
@@ -9,10 +9,8 @@
|
||||
IconArrowRight,
|
||||
Loading,
|
||||
Panel,
|
||||
Scroller,
|
||||
TabItem,
|
||||
TabList,
|
||||
closePopup,
|
||||
fetchMetadataLocalStorage,
|
||||
ticker
|
||||
} from '@hcengineering/ui'
|
||||
@@ -20,6 +18,7 @@
|
||||
import Expandable from '@hcengineering/ui/src/components/Expandable.svelte'
|
||||
import { ObjectPresenter } from '@hcengineering/view-resources'
|
||||
import { onDestroy } from 'svelte'
|
||||
import MetricsInfo from './statistics/MetricsInfo.svelte'
|
||||
|
||||
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
|
||||
const token: string = getMetadata(presentation.metadata.Token) ?? ''
|
||||
@@ -33,12 +32,9 @@
|
||||
let admin = false
|
||||
onDestroy(
|
||||
ticker.subscribe(() => {
|
||||
fetch(endpoint + `/api/v1/statistics?token=${token}`, {}).then(async (json) => {
|
||||
void fetch(endpoint + `/api/v1/statistics?token=${token}`, {}).then(async (json) => {
|
||||
data = await json.json()
|
||||
admin = data?.admin ?? false
|
||||
if (!admin) {
|
||||
closePopup()
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
@@ -86,15 +82,34 @@
|
||||
}
|
||||
employees = emp
|
||||
})
|
||||
const toNum = (value: any) => value as number
|
||||
|
||||
let warningTimeout = 15
|
||||
|
||||
$: metricsData = data?.metrics as Metrics | undefined
|
||||
|
||||
$: totalStats = Array.from(Object.entries(activeSessions).values()).reduce(
|
||||
(cur, it) => {
|
||||
const totalFind = it[1].reduce((it, itm) => itm.current.find + it, 0)
|
||||
const totalTx = it[1].reduce((it, itm) => itm.current.tx + it, 0)
|
||||
return {
|
||||
find: cur.find + totalFind,
|
||||
tx: cur.tx + totalTx
|
||||
}
|
||||
},
|
||||
{ find: 0, tx: 0 }
|
||||
)
|
||||
</script>
|
||||
|
||||
<Panel on:close isFullSize useMaxWidth={true}>
|
||||
<svelte:fragment slot="header">
|
||||
{#if data}
|
||||
Mem: {data.statistics.memoryUsed} / {data.statistics.memoryTotal} CPU: {data.statistics.cpuUsage}
|
||||
<div class="flex-col">
|
||||
<span>
|
||||
Mem: {data.statistics.memoryUsed} / {data.statistics.memoryTotal} CPU: {data.statistics.cpuUsage}
|
||||
</span>
|
||||
<span>
|
||||
TotalFind: {totalStats.find} / Total Tx: {totalStats.tx}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
@@ -118,7 +133,7 @@
|
||||
icon={IconArrowRight}
|
||||
label={getEmbeddedLabel('Set maintenance warning')}
|
||||
on:click={() => {
|
||||
fetch(endpoint + `/api/v1/manage?token=${token}&operation=maintenance&timeout=${warningTimeout}`, {
|
||||
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=maintenance&timeout=${warningTimeout}`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
}}
|
||||
@@ -136,7 +151,7 @@
|
||||
icon={IconArrowRight}
|
||||
label={getEmbeddedLabel('Reboot server')}
|
||||
on:click={() => {
|
||||
fetch(endpoint + `/api/v1/manage?token=${token}&operation=reboot`, {
|
||||
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=reboot`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
}}
|
||||
@@ -151,92 +166,74 @@
|
||||
{@const totalTx = act[1].reduce((it, itm) => itm.current.tx + it, 0)}
|
||||
{@const employeeGroups = Array.from(new Set(act[1].map((it) => it.userId)))}
|
||||
<span class="flex-col">
|
||||
<div class="fs-title">
|
||||
Workspace: {act[0]}: {act[1].length} current 5 mins => {totalFind}/{totalTx}
|
||||
</div>
|
||||
|
||||
<div class="flex-col">
|
||||
{#each employeeGroups as employeeId}
|
||||
{@const employee = employees.get(employeeId)}
|
||||
{@const connections = act[1].filter((it) => it.userId === employeeId)}
|
||||
|
||||
{@const find = connections.reduce((it, itm) => itm.current.find + it, 0)}
|
||||
{@const txes = connections.reduce((it, itm) => itm.current.tx + it, 0)}
|
||||
<div class="p-1 flex-col">
|
||||
<Expandable>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center p-1">
|
||||
{#if employee}
|
||||
<ObjectPresenter
|
||||
_class={contact.mixin.Employee}
|
||||
objectId={employee.person}
|
||||
props={{ shouldShowAvatar: true }}
|
||||
/>
|
||||
{:else}
|
||||
{employeeId}
|
||||
{/if}
|
||||
: {connections.length}
|
||||
<div class="ml-4">
|
||||
<div class="ml-1">{find}/{txes}</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#each connections as user, i}
|
||||
<div class="flex-row-center ml-10">
|
||||
#{i}
|
||||
{user.userId}
|
||||
<div class="p-1">
|
||||
Total: {user.total.find}/{user.total.tx}
|
||||
</div>
|
||||
<div class="p-1">
|
||||
Previous 5 mins: {user.mins5.find}/{user.mins5.tx}
|
||||
</div>
|
||||
<div class="p-1">
|
||||
Current 5 mins: {user.current.find}/{user.current.tx}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1 flex-col ml-10">
|
||||
{#each Object.entries(user.data ?? {}) as [k, v]}
|
||||
<div class="p-1">
|
||||
{k}: {JSON.stringify(v)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</Expandable>
|
||||
<Expandable contentColor expanded={false} expandable={true} bordered>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="fs-title">
|
||||
Workspace: {act[0]}: {act[1].length} current 5 mins => {totalFind}/{totalTx}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="flex-col">
|
||||
{#each employeeGroups as employeeId}
|
||||
{@const employee = employees.get(employeeId)}
|
||||
{@const connections = act[1].filter((it) => it.userId === employeeId)}
|
||||
|
||||
{@const find = connections.reduce((it, itm) => itm.current.find + it, 0)}
|
||||
{@const txes = connections.reduce((it, itm) => itm.current.tx + it, 0)}
|
||||
<div class="p-1 flex-col ml-4">
|
||||
<Expandable>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center p-1">
|
||||
{#if employee}
|
||||
<ObjectPresenter
|
||||
_class={contact.mixin.Employee}
|
||||
objectId={employee.person}
|
||||
props={{ shouldShowAvatar: true, disabled: true }}
|
||||
/>
|
||||
{:else}
|
||||
{employeeId}
|
||||
{/if}
|
||||
: {connections.length}
|
||||
<div class="ml-4">
|
||||
<div class="ml-1">{find}/{txes}</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#each connections as user, i}
|
||||
<div class="flex-row-center ml-10">
|
||||
#{i}
|
||||
{user.userId}
|
||||
<div class="p-1">
|
||||
Total: {user.total.find}/{user.total.tx}
|
||||
</div>
|
||||
<div class="p-1">
|
||||
Previous 5 mins: {user.mins5.find}/{user.mins5.tx}
|
||||
</div>
|
||||
<div class="p-1">
|
||||
Current 5 mins: {user.current.find}/{user.current.tx}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1 flex-col ml-10">
|
||||
{#each Object.entries(user.data ?? {}) as [k, v]}
|
||||
<div class="p-1">
|
||||
{k}: {JSON.stringify(v)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</Expandable>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Expandable>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if selectedTab === 'statistics'}
|
||||
<Scroller>
|
||||
<table class="antiTable" class:highlightRows={true}>
|
||||
<thead class="scroller-thead">
|
||||
<tr>
|
||||
<th><div class="p-1">Name</div> </th>
|
||||
<th>Average</th>
|
||||
<th>Total</th>
|
||||
<th>Ops</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each metricsToRows(data.metrics, 'System') as row}
|
||||
<tr class="antiTable-body__row">
|
||||
<td>
|
||||
<span style={`padding-left: ${toNum(row[0]) + 0.5}rem;`}>
|
||||
{row[1]}
|
||||
</span>
|
||||
</td>
|
||||
<td>{row[2]}</td>
|
||||
<td>{row[3]}</td>
|
||||
<td>{row[4]}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</Scroller>
|
||||
<div class="flex-column p-3 h-full" style:overflow="auto">
|
||||
{#if metricsData !== undefined}
|
||||
<MetricsInfo metrics={metricsData} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Loading />
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import { Metrics } from '@hcengineering/core'
|
||||
import { Expandable } from '@hcengineering/ui'
|
||||
import { FixedColumn } from '@hcengineering/view-resources'
|
||||
|
||||
export let metrics: Metrics
|
||||
export let level = 0
|
||||
export let name: string = 'System'
|
||||
|
||||
$: haschilds = Object.keys(metrics.measurements).length > 0 || Object.keys(metrics.params).length > 0
|
||||
|
||||
function showAvg (name: string, time: number, ops: number): string {
|
||||
if (name.startsWith('#')) {
|
||||
return `➿ ${time}`
|
||||
}
|
||||
if (ops === 0) {
|
||||
return `⏱️ ${time}`
|
||||
}
|
||||
return `${Math.floor((time / ops) * 100) / 100}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<Expandable
|
||||
expanded={level === 0}
|
||||
expandable={level !== 0 && haschilds}
|
||||
bordered
|
||||
showChevron={haschilds && level !== 0}
|
||||
contentColor
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center flex-between flex-grow ml-2">
|
||||
{name}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="tools">
|
||||
<FixedColumn key="row">
|
||||
<div class="flex-row-center flex-between">
|
||||
<FixedColumn key="ops">
|
||||
<span class="p-1">
|
||||
{metrics.operations}
|
||||
</span>
|
||||
</FixedColumn>
|
||||
<FixedColumn key="time">
|
||||
<span class="p-1">
|
||||
{showAvg(name, metrics.value, metrics.operations)}
|
||||
</span>
|
||||
</FixedColumn>
|
||||
<FixedColumn key="time-full">
|
||||
<span class="p-1">
|
||||
{metrics.value}
|
||||
</span>
|
||||
</FixedColumn>
|
||||
</div>
|
||||
</FixedColumn>
|
||||
</svelte:fragment>
|
||||
{#each Object.entries(metrics.measurements) as [k, v], i}
|
||||
<div style:margin-left={`${level * 0.5}rem`}>
|
||||
<svelte:self metrics={v} name="{i}. {k}" level={level + 1} />
|
||||
</div>
|
||||
{/each}
|
||||
{#each Object.entries(metrics.params) as [k, v], i}
|
||||
<div style:margin-left={`${level * 0.5}rem`}>
|
||||
{#each Object.entries(v).toSorted((a, b) => b[1].value / (b[1].operations + 1) - a[1].value / (a[1].operations + 1)) as [kk, vv]}
|
||||
<Expandable expandable={false} bordered showChevron={false} contentColor>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center flex-between flex-grow">
|
||||
# {k} = {kk}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="tools">
|
||||
<FixedColumn key="row">
|
||||
<div class="flex-row-center flex-between">
|
||||
<FixedColumn key="ops">{vv.operations}</FixedColumn>
|
||||
<FixedColumn key="time">{showAvg(kk, vv.value, vv.operations)}</FixedColumn>
|
||||
<FixedColumn key="time-full">{vv.value}</FixedColumn>
|
||||
</div>
|
||||
</FixedColumn>
|
||||
</svelte:fragment>
|
||||
</Expandable>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</Expandable>
|
||||
Reference in New Issue
Block a user