2
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
3
* Licensed under the Apache License, Version 2.0 (the "License");
4
* you may not use this file except in compliance with the License.
5
* You may obtain a copy of the License at
7
* http://www.apache.org/licenses/LICENSE-2.0
9
* Unless required by applicable law or agreed to in writing, software
10
* distributed under the License is distributed on an "AS IS" BASIS,
11
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
* See the License for the specific language governing permissions and
13
* limitations under the License.
16
import { float64, int32 } from "@koalaui/compat"
19
* A probe to measure performance.
21
* A probe can measure performance of any activity which has an entry and an exit points.
22
* Such activity can be a method call, or a sequence of actions, possibly asynchronous.
24
* A probe which has been entered and exited is considered a performed probe (see {@link MainPerfProbe.probePerformed}).
25
* A probe can be entered recursively. When all the recursive calls exits the probe becomes a performed probe.
27
* All performing probes form a hierarchy which is rooted at the main probe (see {@link enterMainPerfProbe}).
28
* A last started probe (see {@link MainPerfProbe.enterProbe}) which has not yet performed becomes a parent
29
* for the next started probe. It's the responsibility of the API caller to keep this parent-child relationship valid,
30
* that is: a child probe should exit before its parent probe exits.
32
* Statistics gathered by a probe:
34
* - recursive call count
35
* - total time and percentage relative to the main (root) probe
37
export interface PerfProbe {
39
* The name of the probe.
44
* Whether this is a dummy probe which does not measure (a noop).
46
* @see MainPerfProbe.getProbe
48
readonly dummy: boolean
53
* @param log log the gathered statistics.
54
* @see MainPerfProbe.enterProbe
56
exit(log: boolean|undefined): void
59
* Cancels measuring the probe and its children probes.
64
* Whether the probe was canceled.
66
readonly canceled: boolean
70
* The main (root) {@link PerfProbe}.
72
* This probe is used to enter the main activity.
74
* Calling {@link PerfProbe.cancel} removes the main probe and disposes all its resources.
76
* Calling {@link PerfProbe.exit} exits the main probe, cancels it and when the log option is provided
77
* logs the gathered statistics.
79
* @see enterMainPerfProbe
80
* @see getMainPerfProbe
82
export interface MainPerfProbe extends PerfProbe {
84
* Enters a child probe referenced by the {@link name} and measures it.
85
* If the probe does not exist, returns a dummy instance.
87
* If the probe already performs a recursive call is counted.
92
enterProbe(name: string): PerfProbe
95
* Exits a child probe referenced by the {@link name}.
96
* If the probe does not exist, returns a dummy instance.
98
* This is an equivalent of calling {@link getProbe} and then {@link PerfProbe.exit}.
100
exitProbe(name: string): PerfProbe
103
* Returns the child probe referenced by the {@link name} if it exists,
104
* otherwise a dummy instance.
106
* @see PerfProbe.dummy
108
getProbe(name: string): PerfProbe
111
* Performs the {@link func} of a child probe referenced by the {@link name} and measures it.
113
* This is an equivalent of calling {@link enterProbe} and then {@link exitProbe}.
115
* If the probe already performs a recursive call is counted.
117
performProbe<T>(name: string, func: () => T): T
120
* Returns true if the probe referenced by the {@link name} has been performed
121
* (entered and exited all the recursive calls).
123
probePerformed(name: string): boolean
127
* Creates a {@link MainPerfProbe} instance with the {@link name} and enters its main probe.
129
* If a {@link MainPerfProbe} with this {@link name} already exists then it is canceled and the new one is created.
131
* Exit it with {@link MainPerfProbe.exit}.
133
export function enterMainPerfProbe(name: string): MainPerfProbe {
134
return new MainPerfProbeImpl(name)
138
* Returns {@link MainPerfProbe} instance with the {@link name} if it exists,
139
* otherwise a dummy instance.
141
* @see MainPerfProbe.dummy
143
export function getMainPerfProbe(name: string): MainPerfProbe {
144
const instance = MainPerfProbeImpl.mainProbes.get(name)
145
return instance ? instance : MainPerfProbeImpl.DUMMY
148
class DummyPerfProbe implements MainPerfProbe {
149
get name(): string { return "dummy" }
150
get dummy(): boolean { return true }
151
exit(log: boolean|undefined): void {}
153
get canceled(): boolean { return false }
154
enterProbe(name: string): PerfProbe { return PerfProbeImpl.DUMMY }
155
exitProbe (name: string): PerfProbe { return PerfProbeImpl.DUMMY }
156
getProbe(name: string): PerfProbe { return PerfProbeImpl.DUMMY }
157
//performProbe: <T>(_: string, func: () => T) => func(),
158
performProbe<T>(name: string, func: () => T): T { return func() }
159
probePerformed(_: string): boolean { return false }
162
class PerfProbeImpl implements PerfProbe {
165
main?: MainPerfProbeImpl,
166
parent?: PerfProbeImpl,
167
remainder: boolean = false
172
this.remainder = remainder
175
private readonly _name: string
176
private readonly _main: MainPerfProbeImpl|undefined
177
public parent: PerfProbeImpl|undefined
178
public readonly remainder: boolean
180
children: Array<PerfProbeImpl> = new Array<PerfProbeImpl>()
181
childrenSorted: boolean = false
183
startTime: float64 = 0.0
184
totalTime: float64 = 0.0
186
currentRecursionDepth: int32 = 0
187
recursiveCallCount: int32 = 0
188
_canceled: boolean = false
194
get dummy(): boolean {
198
get main(): MainPerfProbeImpl {
202
get performing(): boolean {
203
return this.startTime > 0
206
exit(log?: boolean): void {
207
if (this.canceled) return
209
if (this.currentRecursionDepth == 0) {
210
this.totalTime += Date.now() - this.startTime
214
this.currentRecursionDepth--
216
if (!this.performing) {
222
cancel(cancelChildren: boolean = true) {
223
this._canceled = true
224
if (cancelChildren) this.maybeCancelChildren()
227
private maybeCancelChildren() {
228
MainPerfProbeImpl.visit(this, false, (probe: PerfProbeImpl, depth: int32) => {
229
if (probe.performing) probe.cancel(false)
233
get canceled(): boolean {
234
return this._canceled
239
return `[${this.name}] canceled`
241
if (this.performing) {
242
return `[${this.name}] still performing...`
244
const mainProbe = this.main.probes.get(this.main.name)!
245
const percentage = mainProbe.totalTime > 0 ? Math.round((100 / mainProbe.totalTime) * this.totalTime) : 0
246
return `[${this.name}] call count: ${this.callCount}` +
247
` | recursive call count: ${this.recursiveCallCount}` +
248
` | time: ${this.totalTime}ms ${percentage}%`
251
protected log(prefix?: string) {
252
console.log(prefix ? `${prefix}${this.toString()}` : this.toString())
255
static readonly DUMMY: PerfProbe = new DummyPerfProbe()
258
class MainPerfProbeImpl extends PerfProbeImpl implements MainPerfProbe {
263
const prev = MainPerfProbeImpl.mainProbes.get(name)
264
if (prev) prev.cancel()
265
MainPerfProbeImpl.mainProbes.set(name, this)
266
this.currentProbe = this.enterProbe(name)
269
static readonly mainProbes: Map<string, MainPerfProbeImpl> = new Map<string, MainPerfProbeImpl>()
271
currentProbe: PerfProbeImpl
272
probes: Map<string, PerfProbeImpl> = new Map<string, PerfProbeImpl>()
274
private createProbe(name: string): PerfProbeImpl {
275
const probes = name == this.name ? this : new PerfProbeImpl(name, this)
280
get main(): MainPerfProbeImpl {
284
push(probe: PerfProbeImpl) {
285
probe.parent = this.currentProbe
286
probe.index = probe.parent ? probe.parent!.children.length as int32 : 0
287
if (probe.parent) probe.parent!.children.push(probe)
288
this.currentProbe = probe
291
pop(probe: PerfProbeImpl) {
293
this.currentProbe = probe.parent!
297
performProbe<T>(name: string, func: () => T): T {
298
const probe = this.enterProbe(name)
307
enterProbe(name: string): PerfProbeImpl {
308
let probe = this.probes.get(name)
310
probe = this.createProbe(name)
311
this.probes.set(name, probe)
313
probe._canceled = false
315
if (probe.performing) {
316
probe.recursiveCallCount++
317
probe.currentRecursionDepth++
319
probe.startTime = Date.now()
325
exitProbe(name: string): PerfProbe {
326
const probe = this.getProbe(name)
327
probe.exit(undefined)
331
getProbe(name: string): PerfProbe {
332
const probe = this.probes.get(name)
333
return probe !== undefined ? probe : PerfProbeImpl.DUMMY
336
probePerformed(name: string): boolean {
337
const probe = this.probes.get(name)
338
return probe != undefined && !probe!.performing && !probe!.canceled
341
exit(log?: boolean) {
348
MainPerfProbeImpl.mainProbes.delete(this.name)
351
static visit(root: PerfProbeImpl, logging: boolean, apply: (probe: PerfProbeImpl, depth: int32) => void) {
352
let current: PerfProbeImpl = root
357
if (visiting) apply(current, depth)
358
if (index >= current.children.length) {
359
if (!current.parent) {
362
current = current.parent!
363
index = ++current.index
369
if (logging && !current.childrenSorted) {
370
current.childrenSorted = true
371
current.children = current.children.sort((p1: PerfProbeImpl, p2: PerfProbeImpl):number => p2.totalTime - p1.totalTime)
373
// a special probe to log the time remained
374
current.children.push(new PerfProbeImpl("<remainder>", root.main, current, true))
377
current = current.children[index]
383
protected log(prefix?: string) {
384
prefix = prefix !== undefined ? `${prefix}: ` : ""
385
console.log(`${prefix}perf probes:`)
387
MainPerfProbeImpl.visit(this.main, true, (probe, depth):void => {
389
for (let i = 0; i < depth; i++) indent += "\t"
390
if (probe.remainder) {
391
const parentTime = probe.parent!.totalTime
393
probe.parent!.children.forEach((a: PerfProbeImpl):void => { childrenTime += a.totalTime })
394
probe.totalTime = Math.max(0, parentTime - childrenTime) as int32
395
const percentage = parentTime > 0 ? Math.round((100 / parentTime) * probe.totalTime) : 0
396
console.log(`${prefix}${indent}[${probe.name}] time: ${probe.totalTime}ms ${percentage}%`)
399
console.log(`${prefix}${indent}${probe.toString()}`)
404
static readonly DUMMY: MainPerfProbe = new DummyPerfProbe()