UNPKG

@itwin/frontend-devtools

Version:

Debug menu and supporting UI widgets

196 lines • 8.14 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Widgets */ import { saveAs } from "file-saver"; import { IModelApp } from "@itwin/core-frontend"; import { createCheckBox } from "../ui/CheckBox"; const dummyArgs = { 0: 0 }; // Reuse instead of allocating for each entry /** * @param name Label for the trace event * @param start Timestamp in microseconds of when trace event started * @param duration Duration in microseconds of trace event */ function createTraceEvent(name, start, duration) { return { pid: 1, ts: start, dur: duration, ph: "X", name, args: dummyArgs, }; } function createTraceFromTimerResults(timerResults) { const traceEvents = []; const addChildren = (startTime, children) => { for (const child of children) { if (child.nanoseconds < 100) continue; const microseconds = child.nanoseconds / 1E3; traceEvents.push(createTraceEvent(child.label, startTime, microseconds)); if (child.children) addChildren(startTime, child.children); startTime += microseconds; } }; let frameStartTime = 0; let frameNumber = 0; for (const tr of timerResults) { const microseconds = tr.nanoseconds / 1E3; traceEvents.push(createTraceEvent(`Frame ${frameNumber}`, frameStartTime, microseconds)); if (tr.children) addChildren(frameStartTime, tr.children); frameStartTime += microseconds; ++frameNumber; } return { traceEvents }; } /** @alpha */ export class GpuProfiler { _div; _resultsDiv; _results; _debugControl; _recordButton; _recordedResults; _isRecording; constructor(parent) { this._debugControl = IModelApp.renderSystem.debugControl; const checkBox = createCheckBox({ parent, name: "Profile GPU", id: "gpu-profiler-toggle", handler: (cb) => this.toggleProfileCheckBox(cb.checked), }); if (!this._debugControl.isGLTimerSupported) { checkBox.checkbox.disabled = true; checkBox.div.title = "EXT_disjoint_timer_query is not available in this browser"; } this._div = document.createElement("div"); this._div.style.display = "none"; this._recordButton = document.createElement("button"); this._recordButton.style.textAlign = "center"; this._isRecording = false; this._recordButton.innerText = "Record Profile"; this._recordButton.title = "Record a profile to open with chrome://tracing"; this._recordedResults = []; this._recordButton.addEventListener("click", this._clickRecord); this._div.appendChild(this._recordButton); this._results = []; this._resultsDiv = document.createElement("div"); this._resultsDiv.style.textAlign = "left"; this._div.appendChild(this._resultsDiv); parent.appendChild(this._div); } [Symbol.dispose]() { this._debugControl.resultsCallback = undefined; } toggleProfileCheckBox(isEnabled) { if (isEnabled) { this._debugControl.resultsCallback = this._resultsCallback; this._resultsDiv.innerHTML = ""; this._div.style.display = "block"; } else { this._debugControl.resultsCallback = undefined; this._div.style.display = "none"; this.stopRecording(); } } _clickRecord = () => { if (!this._isRecording) { this._isRecording = true; this._recordButton.innerText = "Stop Recording"; return; } this.stopRecording(); }; stopRecording() { this._isRecording = false; this._recordButton.innerText = "Record Profile"; if (this._recordedResults.length !== 0) { const chromeTrace = createTraceFromTimerResults(this._recordedResults); const blob = new Blob([JSON.stringify(chromeTrace)], { type: "application/json;charset=utf-8" }); saveAs(blob, "gpu-profile.json"); this._recordedResults = []; } } _resultsCallback = (result) => { if (this._isRecording) this._recordedResults.push(result); const fragment = document.createDocumentFragment(); const numSavedFrames = 120; let lastValue; const changedResults = new Array(this._results.length); // default values false const printDepth = (depth, currentRes) => { const index = this._results.findIndex((res) => res.label === currentRes.label); if (index < 0) { // Add brand new entry const data = { label: currentRes.label, paddingLeft: `${depth}em`, sum: currentRes.nanoseconds, values: [currentRes.nanoseconds], }; if (lastValue === undefined) { this._results.unshift(data); changedResults.unshift(true); } else if (currentRes.label === "Read Pixels") { this._results.push(data); // Read Pixels should go at the end of the list changedResults.push(true); } else { const prevIndex = this._results.findIndex((res) => res.label === lastValue); this._results.splice(prevIndex + 1, 0, data); changedResults.splice(prevIndex + 1, 0, true); } } else { // Edit old entry let oldVal = 0.0; const savedResults = this._results[index]; if (savedResults.values.length >= numSavedFrames) { // keep up to numSavedFrames values to average between oldVal = savedResults.values.shift(); } const newVal = currentRes.nanoseconds < 100 ? 0.0 : currentRes.nanoseconds; // high-pass filter, empty queries have some noise savedResults.sum += newVal - oldVal; savedResults.values.push(newVal); changedResults[index] = true; } lastValue = currentRes.label; if (!currentRes.children) return; for (const childRes of currentRes.children) printDepth(depth + 1, childRes); }; printDepth(0, result); this._results.forEach((value, index) => { if (!changedResults[index]) { // if no data received on this item, add a value of 0.0 to the avg. const oldVal = value.values.length >= numSavedFrames ? value.values.shift() : 0.0; value.sum -= oldVal; value.values.push(0.0); } const div = document.createElement("div"); div.style.display = "flex"; div.style.width = "75%"; const textLabel = document.createElement("text"); textLabel.innerText = `${value.label}`; textLabel.style.paddingLeft = value.paddingLeft; div.appendChild(textLabel); const divLine = document.createElement("div"); divLine.style.flexGrow = "1"; divLine.style.borderBottom = "dotted 1px"; div.appendChild(divLine); const textValue = document.createElement("text"); textValue.innerText = `${(value.sum / value.values.length / 1.E6).toFixed(3)} ms\n`; div.appendChild(textValue); fragment.appendChild(div); }); this._resultsDiv.innerHTML = ""; this._resultsDiv.appendChild(fragment); }; } //# sourceMappingURL=GpuProfiler.js.map