• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-2025 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
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
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.
14 */
15
16import { float64, int32, timeNow, numberToFixed } from "@koalaui/compat"
17
18/**
19 * A probe to measure performance.
20 *
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.
23 *
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.
26 *
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.
31 *
32 * Statistics gathered by a probe:
33 * - call count
34 * - recursive call count
35 * - total time and percentage relative to the main (root) probe
36 */
37export interface PerfProbe {
38    /**
39     * The name of the probe.
40     */
41    readonly name: string
42
43    /**
44     * Whether this is a dummy probe which does not measure (a noop).
45     *
46     * @see MainPerfProbe.getProbe
47     */
48    readonly dummy: boolean
49
50    /**
51     * Exists the probe.
52     *
53     * @param log log the gathered statistics.
54     * @see MainPerfProbe.enterProbe
55     */
56    exit(log: boolean|undefined): void
57
58    /**
59     * Cancels measuring the probe and its children probes.
60     */
61    cancel(): void
62
63    /**
64     * User-defined data associated with the probe.
65     */
66    userData: string | undefined
67
68    /**
69     * Whether the probe was canceled.
70     */
71    readonly canceled: boolean
72}
73
74/**
75 * The main (root) {@link PerfProbe}.
76 *
77 * This probe is used to enter the main activity.
78 *
79 * Calling {@link PerfProbe.cancel} removes the main probe and disposes all its resources.
80 *
81 * Calling {@link PerfProbe.exit} exits the main probe, cancels it and when the log option is provided
82 * logs the gathered statistics.
83 *
84 * @see enterMainPerfProbe
85 * @see getMainPerfProbe
86 */
87export interface MainPerfProbe extends PerfProbe {
88    /**
89     * Enters a child probe referenced by the {@link name} and measures it.
90     * If the probe does not exist, returns a dummy instance.
91     *
92     * If the probe already performs a recursive call is counted.
93     *
94     * @see PerfProbe.exit
95     * @see exitProbe
96     */
97    enterProbe(name: string): PerfProbe
98
99    /**
100     * Exits a child probe referenced by the {@link name}.
101     * If the probe does not exist, returns a dummy instance.
102     *
103     * This is an equivalent of calling {@link getProbe} and then {@link PerfProbe.exit}.
104     */
105    exitProbe(name: string): PerfProbe
106
107    /**
108     * Returns the child probe referenced by the {@link name} if it exists,
109     * otherwise a dummy instance.
110     *
111     * @see PerfProbe.dummy
112     */
113    getProbe(name: string): PerfProbe
114
115    /**
116     * Performs the {@link func} of a child probe referenced by the {@link name} and measures it.
117     *
118     * This is an equivalent of calling {@link enterProbe} and then {@link exitProbe}.
119     *
120     * If the probe already performs a recursive call is counted.
121     */
122    performProbe<T>(name: string, func: () => T): T
123
124    /**
125     * Returns true if the probe referenced by the {@link name} has been performed
126     * (entered and exited all the recursive calls).
127     */
128    probePerformed(name: string): boolean
129}
130
131/**
132 * Creates a {@link MainPerfProbe} instance with the {@link name} and enters its main probe.
133 *
134 * If a {@link MainPerfProbe} with this {@link name} already exists then it is canceled and the new one is created.
135 *
136 * Exit it with {@link MainPerfProbe.exit}.
137 */
138export function enterMainPerfProbe(name: string): MainPerfProbe {
139    return new MainPerfProbeImpl(name)
140}
141
142/**
143 * Returns {@link MainPerfProbe} instance with the {@link name} if it exists,
144 * otherwise a dummy instance.
145 *
146 * @see MainPerfProbe.dummy
147 */
148export function getMainPerfProbe(name: string): MainPerfProbe {
149    const instance = MainPerfProbeImpl.mainProbes.get(name)
150    return instance ? instance : MainPerfProbeImpl.DUMMY
151}
152
153class DummyPerfProbe implements MainPerfProbe {
154    get name(): string { return "dummy" }
155    get dummy(): boolean { return true }
156    exit(log: boolean|undefined): void {}
157    cancel () {}
158    get canceled(): boolean { return false }
159    enterProbe(name: string): PerfProbe { return PerfProbeImpl.DUMMY }
160    exitProbe (name: string): PerfProbe { return PerfProbeImpl.DUMMY }
161    getProbe(name: string): PerfProbe { return PerfProbeImpl.DUMMY }
162    //performProbe: <T>(_: string, func: () => T) => func(),
163    performProbe<T>(name: string, func: () => T): T { return func() }
164    probePerformed(_: string): boolean { return false }
165
166    get userData(): string | undefined {
167        return undefined
168    }
169    set userData(_: string | undefined) {}
170}
171
172class PerfProbeImpl implements PerfProbe {
173    constructor(
174        name: string,
175        main?: MainPerfProbeImpl,
176        parent?: PerfProbeImpl,
177        remainder: boolean = false
178    ) {
179        this._name = name
180        this._main = main
181        this.parent = parent
182        this.remainder = remainder
183    }
184
185    private readonly _name: string
186    private readonly _main: MainPerfProbeImpl|undefined
187    public parent: PerfProbeImpl|undefined
188    public readonly remainder: boolean
189
190    children: Array<PerfProbeImpl> = new Array<PerfProbeImpl>()
191    childrenSorted: boolean = false
192    index: int32 = 0
193    startTime: float64 = 0.0
194    totalTime: float64 = 0.0
195    callCount: int32 = 0
196    currentRecursionDepth: int32 = 0
197    recursiveCallCount: int32 = 0
198    _canceled: boolean = false
199    private _userData?: string
200
201    get name(): string {
202        return this._name
203    }
204
205    get dummy(): boolean {
206        return false
207    }
208
209    get main(): MainPerfProbeImpl {
210        return this._main!
211    }
212
213    get performing(): boolean {
214        return this.startTime > 0
215    }
216
217    get userData(): string | undefined {
218        return this._userData
219    }
220
221    set userData(value: string | undefined) {
222        this._userData = value
223    }
224
225    exit(log?: boolean): void {
226        if (this.canceled) return
227
228        if (this.currentRecursionDepth == 0) {
229            this.totalTime += timeNow() - this.startTime
230            this.startTime = 0
231        } else {
232            this.currentRecursionDepth--
233        }
234        if (!this.performing) {
235            this.main.pop(this)
236        }
237        if (log) this.log()
238    }
239
240    cancel(cancelChildren: boolean = true) {
241        this._canceled = true
242        if (cancelChildren) this.maybeCancelChildren()
243    }
244
245    private maybeCancelChildren() {
246        MainPerfProbeImpl.visit(this, false, (probe: PerfProbeImpl, depth: int32) => {
247            if (probe.performing) probe.cancel(false)
248        })
249    }
250
251    get canceled(): boolean {
252        return this._canceled
253    }
254
255    toString(): string {
256        if (this.canceled) {
257            return `[${this.name}] canceled`
258        }
259        if (this.performing) {
260            return `[${this.name}] still performing...`
261        }
262        const mainProbe = this.main.probes.get(this.main.name)!
263        const percentage = mainProbe.totalTime > 0 ? Math.round((100 / mainProbe.totalTime) * this.totalTime) : 0
264
265        let result = `[${this.name}] call count: ${this.callCount}`
266            +  ` | recursive call count: ${this.recursiveCallCount}`
267            +  ` | time: ${this.totalTime}ms ${percentage}%`
268
269        if (this.userData) {
270            result += ` | user data: ${this.userData}`
271        }
272
273        return result
274    }
275
276    protected log(prefix?: string) {
277        console.log(prefix ? `${prefix}${this.toString()}` : this.toString())
278    }
279
280    static readonly DUMMY: PerfProbe = new DummyPerfProbe()
281}
282
283class MainPerfProbeImpl extends PerfProbeImpl implements MainPerfProbe {
284    constructor(
285        name: string
286    ) {
287        super(name)
288        const prev = MainPerfProbeImpl.mainProbes.get(name)
289        if (prev) prev.cancel()
290        MainPerfProbeImpl.mainProbes.set(name, this)
291        this.currentProbe = this.enterProbe(name)
292    }
293
294    static readonly mainProbes: Map<string, MainPerfProbeImpl> = new Map<string, MainPerfProbeImpl>()
295
296    currentProbe: PerfProbeImpl
297    probes: Map<string, PerfProbeImpl> = new Map<string, PerfProbeImpl>()
298
299    private createProbe(name: string): PerfProbeImpl {
300        const probes = name == this.name ? this : new PerfProbeImpl(name, this)
301        this.push(probes)
302        return probes
303    }
304
305    get main(): MainPerfProbeImpl {
306        return this
307    }
308
309    push(probe: PerfProbeImpl) {
310        probe.parent = this.currentProbe
311        probe.index = probe.parent ? probe.parent!.children.length as int32 : 0
312        if (probe.parent) probe.parent!.children.push(probe)
313        this.currentProbe = probe
314    }
315
316    pop(probe: PerfProbeImpl) {
317        if (probe.parent) {
318            this.currentProbe = probe.parent!
319        }
320    }
321
322    performProbe<T>(name: string, func: () => T): T {
323        const probe = this.enterProbe(name)
324        try {
325            return func()
326        } finally {
327            probe.exit()
328        }
329    }
330
331    enterProbe(name: string): PerfProbeImpl {
332        let probe = this.probes.get(name)
333        if (!probe) {
334            probe = this.createProbe(name)
335            this.probes.set(name, probe)
336        }
337        probe._canceled = false
338
339        if (probe.performing) {
340            probe.recursiveCallCount++
341            probe.currentRecursionDepth++
342        } else {
343            probe.startTime = timeNow()
344            probe.callCount++
345        }
346        return probe
347    }
348
349    exitProbe(name: string): PerfProbe {
350        const probe = this.getProbe(name)
351        probe.exit(undefined)
352        return probe
353    }
354
355    getProbe(name: string): PerfProbe {
356        const probe = this.probes.get(name)
357        return probe !== undefined ? probe : PerfProbeImpl.DUMMY
358    }
359
360    probePerformed(name: string): boolean {
361        const probe = this.probes.get(name)
362        return probe != undefined && !probe!.performing && !probe!.canceled
363    }
364
365    exit(log?: boolean) {
366        super.exit()
367        if (log) this.log()
368        this.cancel()
369    }
370
371    cancel() {
372        MainPerfProbeImpl.mainProbes.delete(this.name)
373    }
374
375    static visit(root: PerfProbeImpl, logging: boolean, apply: (probe: PerfProbeImpl, depth: int32) => void) {
376        let current: PerfProbeImpl = root
377        let index = 0
378        let depth = 0
379        let visiting = true
380        while (true) {
381            if (visiting) {
382            	current.index = 0
383            	apply(current, depth)
384            }
385            if (index >= current.children.length) {
386                if (!current.parent) {
387                    break
388                }
389                current = current.parent!
390                index = ++current.index
391                depth--
392                visiting = false
393                continue
394            }
395            visiting = true
396            if (logging && !current.childrenSorted) {
397                current.childrenSorted = true
398                current.children = current.children.sort((p1: PerfProbeImpl, p2: PerfProbeImpl):number => p2.totalTime - p1.totalTime)
399                if (depth == 0) {
400                    // a special probe to log the time remained
401                    current.children.push(new PerfProbeImpl("<remainder>", root.main, current, true))
402                }
403            }
404            current = current.children[index]
405            index = 0
406            depth++
407        }
408    }
409
410    protected log(prefix?: string) {
411        prefix = prefix !== undefined ? `${prefix}: ` : ""
412        console.log(`${prefix}perf probes:`)
413
414        MainPerfProbeImpl.visit(this.main, true, (probe, depth):void => {
415            let indent = "\t"
416            for (let i = 0; i < depth; i++) indent += "\t"
417            if (probe.remainder) {
418                const parentTime = probe.parent!.totalTime
419                let childrenTime = 0
420                probe.parent!.children.forEach((a: PerfProbeImpl):void => { childrenTime += a.totalTime })
421                probe.totalTime = Math.max(0, parentTime - childrenTime) as int32
422                const percentage = parentTime > 0 ? Math.round((100 / parentTime) * probe.totalTime) : 0
423                console.log(`${prefix}${indent}[${probe.name}] time: ${numberToFixed(probe.totalTime, 2)}ms ${percentage}%`)
424            } else {
425                console.log(`${prefix}${indent}${probe.toString()}`)
426            }
427        })
428    }
429
430    static readonly DUMMY: MainPerfProbe = new DummyPerfProbe()
431}
432