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