| // 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(); |