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 { int32 } from "@koalaui/compat" 17 18/** 19 * Adds statistics for constructing/disposing of the TreeNode instances. 20 * It is disabled by default because collecting such data affects performance. 21 */ 22const DEBUG_WITH_NODE_STATS = false 23 24export class KoalaProfiler { 25 private static readonly map = DEBUG_WITH_NODE_STATS 26 ? new Map<int32, Set<Object>>() 27 : undefined 28 29 static nodeCreated(nodeType: int32, node: Object) { 30 if (KoalaProfiler.map === undefined) return 31 let set = KoalaProfiler.map!.get(nodeType) 32 if (set === undefined) { 33 set = new Set<Object>() 34 KoalaProfiler.map!.set(nodeType, set) 35 } 36 set.add(node) 37 } 38 39 static nodeDisposed(nodeType: int32, node: Object) { 40 if (KoalaProfiler.map === undefined) return 41 let set = KoalaProfiler.map!.get(nodeType) 42 if (set === undefined) throw new Error("node never existed") 43 if (!set.delete(node)) console.log("node is already disposed") 44 } 45 46 public static counters: KoalaProfiler | undefined = undefined 47 48 private invalidations = 0 49 private computes = 0 50 private builds = 0 51 private nodes = 0 52 private realDraws = 0 53 private cachedDraws = 0 54 private measures = 0 55 private layouts = 0 56 private frames = 0 57 private lastTime = 0.0 58 private lastFPS = 0 59 private updateEnterTime = 0.0 60 private updateExitTime = 0.0 61 private updateTime = 0.0 62 private buildEnterTime = 0.0 63 private buildExitTime = 0.0 64 private buildTime = 0.0 65 private layoutEnterTime = 0.0 66 private layoutExitTime = 0.0 67 private layoutTime = 0.0 68 private drawEnterTime = 0.0 69 private drawExitTime = 0.0 70 private drawTime = 0.0 71 private updatableStates = 0 72 private mutableStates = 0 73 private computableValues = 0 74 75 static enable() { 76 KoalaProfiler.counters = new KoalaProfiler() 77 } 78 79 static disable() { 80 KoalaProfiler.counters = undefined 81 } 82 83 static enabled(): boolean { 84 return KoalaProfiler.counters != undefined 85 } 86 87 reset() { 88 this.invalidations = 0 89 this.computes = 0 90 this.builds = 0 91 this.nodes = 0 92 this.realDraws = 0 93 this.cachedDraws = 0 94 this.layouts = 0 95 this.measures = 0 96 this.updateEnterTime = 0 97 this.updateExitTime = 0 98 this.updatableStates = 0 99 this.mutableStates = 0 100 this.computableValues = 0 101 } 102 103 report() { 104 console.log(this.getReport()) 105 } 106 107 getReport(): string { 108 const updateTime = Math.round(1000 * (this.updateExitTime - this.updateEnterTime)) 109 const buildTime = Math.round(1000 * (this.buildExitTime - this.buildEnterTime)) 110 const layoutTime = Math.round(1000 * (this.layoutExitTime - this.layoutEnterTime)) 111 const drawTime = Math.round(1000 * (this.drawExitTime - this.drawEnterTime)) 112 if (this.updateTime < updateTime) this.updateTime = updateTime 113 if (this.buildTime < buildTime) this.buildTime = buildTime 114 if (this.layoutTime < layoutTime) this.layoutTime = layoutTime 115 if (this.drawTime < drawTime) this.drawTime = drawTime 116 117 // TODO: OHOS does not properly handle \n in template literals 118 const array = Array.of<string>( 119 `invalidations: ${this.invalidations}`, 120 `modified states: ${this.mutableStates}/${this.updatableStates} + ${this.computableValues}`, 121 `update states (mks): ${this.updateTime} / ${updateTime}`, 122 `build root node (mks): ${this.buildTime} / ${buildTime}`, 123 `layout view (mks): ${this.layoutTime} / ${layoutTime}`, 124 `draw view (mks): ${this.drawTime} / ${drawTime}`, 125 `computes: ${this.computes}`, 126 `builds: ${this.builds}`, 127 `nodes: ${this.nodes}`, 128 `realDraws: ${this.realDraws}`, 129 `cachedDraws: ${this.cachedDraws}`, 130 `measures: ${this.measures}`, 131 `layouts: ${this.layouts}`, 132 `FPS: ${this.lastFPS}`, 133 ) 134 KoalaProfiler.map?.forEach((set:Set<Object>, kind:int32) => { 135 if (set.size > 0) array.push(kind + ":" + set.size) 136 }) 137 return array.join("\n") 138 } 139 140 invalidation() { this.invalidations++ } 141 compute() { this.computes++ } 142 build() { this.builds++ } 143 node() { this.nodes++ } 144 realDraw() { this.realDraws++ } 145 cachedDraw() { this.cachedDraws++ } 146 layout() { this.layouts++ } 147 measure() { this.measures++ } 148 frame(ms: number) { 149 if (ms - this.lastTime <= 1000) { 150 this.frames++ 151 } else { 152 this.lastFPS = Math.round(this.frames * 1000 / (ms - this.lastTime)) as int32 153 this.frames = 1 154 this.lastTime = ms 155 } 156 } 157 buildRootEnter() { 158 this.buildEnterTime = Date.now() 159 } 160 buildRootExit() { 161 this.buildExitTime = Date.now() 162 } 163 layoutEnter() { 164 this.layoutEnterTime = Date.now() 165 } 166 layoutExit() { 167 this.layoutExitTime = Date.now() 168 } 169 drawEnter() { 170 this.drawEnterTime = Date.now() 171 } 172 drawExit() { 173 this.drawExitTime = Date.now() 174 } 175 updateSnapshotEnter() { 176 this.updateEnterTime = Date.now() 177 } 178 updateSnapshotExit() { 179 this.updateExitTime = Date.now() 180 } 181 updateSnapshot(modified: int32, all?: int32) { 182 if (all === undefined) { 183 this.computableValues = modified - this.mutableStates 184 185 } else { 186 this.mutableStates = modified 187 this.updatableStates = all 188 } 189 } 190} 191