// Copyright (C) 2018 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; const hooks = { isDebug: () => false, toggleDebug: () => {}, }; export function setPerfHooks(isDebug: () => boolean, toggleDebug: () => void) { hooks.isDebug = isDebug; hooks.toggleDebug = toggleDebug; } // Shorthand for if globals perf debug mode is on. export const perfDebug = () => hooks.isDebug(); // Returns performance.now() if perfDebug is enabled, otherwise 0. // This is needed because calling performance.now is generally expensive // and should not be done for every frame. export const debugNow = () => (perfDebug() ? performance.now() : 0); // Returns execution time of |fn| if perf debug mode is on. Returns 0 otherwise. export function measure(fn: () => void): number { const start = debugNow(); fn(); return debugNow() - start; } // Stores statistics about samples, and keeps a fixed size buffer of most recent // samples. export class RunningStatistics { private _count = 0; private _mean = 0; private _lastValue = 0; private _ptr = 0; private buffer: number[] = []; constructor(private _maxBufferSize = 10) {} addValue(value: number) { this._lastValue = value; if (this.buffer.length >= this._maxBufferSize) { this.buffer[this._ptr++] = value; if (this._ptr >= this.buffer.length) { this._ptr -= this.buffer.length; } } else { this.buffer.push(value); } this._mean = (this._mean * this._count + value) / (this._count + 1); this._count++; } get mean() { return this._mean; } get count() { return this._count; } get bufferMean() { return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length; } get bufferSize() { return this.buffer.length; } get maxBufferSize() { return this._maxBufferSize; } get last() { return this._lastValue; } } // Returns a summary string representation of a RunningStatistics object. export function runningStatStr(stat: RunningStatistics) { return ( `Last: ${stat.last.toFixed(2)}ms | ` + `Avg: ${stat.mean.toFixed(2)}ms | ` + `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms` ); } export interface PerfStatsSource { renderPerfStats(): m.Children; } // Globals singleton class that renders performance stats for the whole app. class PerfDisplay { private containers: PerfStatsSource[] = []; addContainer(container: PerfStatsSource) { this.containers.push(container); } removeContainer(container: PerfStatsSource) { const i = this.containers.indexOf(container); this.containers.splice(i, 1); } renderPerfStats(src: PerfStatsSource) { if (!perfDebug()) return; const perfDisplayEl = document.querySelector('.perf-stats'); if (!perfDisplayEl) return; m.render(perfDisplayEl, [ m('section', src.renderPerfStats()), m( 'button.close-button', { onclick: hooks.toggleDebug, }, m('i.material-icons', 'close'), ), this.containers.map((c, i) => m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()), ), ]); } } export const perfDisplay = new PerfDisplay();