-
Notifications
You must be signed in to change notification settings - Fork 208
/
GLTimer.ts
190 lines (154 loc) · 6.61 KB
/
GLTimer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/*---------------------------------------------------------------------------------------------
* Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license terms.
*--------------------------------------------------------------------------------------------*/
import { System } from "./System";
import { GLTimerResult, GLTimerResultCallback } from "./../System";
import { BentleyStatus } from "@bentley/bentleyjs-core";
import { IModelError } from "@bentley/imodeljs-common";
class DisjointTimerExtension {
private _e: any; // EXT_disjoint_timer_query, not available in lib.dom.d.ts
private _context: WebGLRenderingContext;
public constructor(system: System) {
this._e = system.capabilities.queryExtensionObject<any>("EXT_disjoint_timer_query");
this._context = system.context;
}
public get isSupported(): boolean { return this._e !== undefined; }
public didDisjointEventHappen(): boolean {
return this._context.getParameter(this._e.GPU_DISJOINT_EXT);
}
public createQuery() { return this._e.createQueryEXT() as WebGLObject; }
public deleteQuery(q: WebGLObject) { this._e.deleteQueryEXT(q); }
public beginQuery(q: WebGLObject) { this._e.beginQueryEXT(this._e.TIME_ELAPSED_EXT, q); }
public endQuery() { this._e.endQueryEXT(this._e.TIME_ELAPSED_EXT); }
public isResultAvailable(q: WebGLObject): boolean {
return this._e.getQueryObjectEXT(q, this._e.QUERY_RESULT_AVAILABLE_EXT);
}
public getResult(q: WebGLObject): number {
return this._e.getQueryObjectEXT(q, this._e.QUERY_RESULT_EXT);
}
}
interface QueryEntry {
label: string;
query: WebGLObject;
children?: QueryEntry[];
}
/** Record GPU hardware queries to profile independent of CPU.
*
* This is a wrapper around EXT_disjoint_timer_query. The extension should be available in the following browsers:
* * Chrome 67 and later
* * Chromium-based Edge
* * Firefox (with webgl.enable-privileged-extensions set to true in about:config)
*
* EXT_disjoint_timer_query only supports one active query per context without nesting. This wrapper keeps an internal stack to make
* nesting work.
*
* The extension API makes timestamps look like a better solution than disjoint timers, but they are not actually supported.
* See https://bugs.chromium.org/p/chromium/issues/detail?id=595172
* @internal
*/
export class GLTimer {
private _extension: DisjointTimerExtension;
private _queryStack: QueryEntry[];
private _resultsCallback?: GLTimerResultCallback;
private constructor(system: System) {
this._extension = new DisjointTimerExtension(system);
this._queryStack = [];
this._resultsCallback = undefined;
}
// This class is necessarily a singleton per context because of the underlying extension it wraps.
// System is expected to call create in its constructor.
public static create(system: System): GLTimer {
return new GLTimer(system);
}
public get isSupported(): boolean { return this._extension.isSupported; }
public set resultsCallback(callback: GLTimerResultCallback | undefined) {
if (this._queryStack.length !== 0)
throw new IModelError(BentleyStatus.ERROR, "Do not set resultsCallback when a frame is already being drawn");
this._resultsCallback = callback;
}
public beginOperation(label: string) {
if (!this._resultsCallback)
return;
this.pushQuery(label);
}
public endOperation() {
if (!this._resultsCallback)
return;
if (this._queryStack.length === 0)
throw new IModelError(BentleyStatus.ERROR, "Mismatched calls to beginOperation/endOperation");
this.popQuery();
}
public beginFrame() {
if (!this._resultsCallback)
return;
if (this._queryStack.length !== 0)
throw new IModelError(BentleyStatus.ERROR, "Already recording timing for a frame");
const query = this._extension.createQuery();
this._extension.beginQuery(query);
this._queryStack.push({ label: "Total", query, children: [] });
}
public endFrame() {
if (!this._resultsCallback)
return;
if (this._queryStack.length !== 1)
throw new IModelError(BentleyStatus.ERROR, "Missing at least one endOperation call");
this._extension.endQuery();
const root = this._queryStack.pop()!;
const userCallback = this._resultsCallback;
const queryCallback = () => {
if (this._extension.didDisjointEventHappen()) {
// Have to throw away results for this frame after disjoint event occurs.
this.cleanupAfterDisjointEvent(root);
return;
}
// It takes more one or more frames for results to become available.
// Only checking time for root since it will always be the last query completed.
if (!this._extension.isResultAvailable(root.query)) {
setTimeout(queryCallback, 0);
return;
}
const processQueryEntry = (queryEntry: QueryEntry): GLTimerResult => {
const time = this._extension.getResult(queryEntry.query);
this._extension.deleteQuery(queryEntry.query);
const result: GLTimerResult = { label: queryEntry.label, nanoseconds: time };
if (queryEntry.children === undefined)
return result;
result.children = [];
for (const child of queryEntry.children) {
const childResult = processQueryEntry(child);
result.children.push(childResult);
result.nanoseconds += childResult.nanoseconds;
}
return result;
};
userCallback(processQueryEntry(root));
};
setTimeout(queryCallback, 0);
}
private cleanupAfterDisjointEvent(queryEntry: QueryEntry) {
this._extension.deleteQuery(queryEntry.query);
if (!queryEntry.children)
return;
for (const child of queryEntry.children)
this.cleanupAfterDisjointEvent(child);
}
private pushQuery(label: string) {
this._extension.endQuery();
const query = this._extension.createQuery();
this._extension.beginQuery(query);
const activeQuery = this._queryStack[this._queryStack.length - 1];
const queryEntry: QueryEntry = { label, query };
this._queryStack.push(queryEntry);
if (activeQuery.children === undefined)
activeQuery.children = [queryEntry];
else
activeQuery.children.push(queryEntry);
}
private popQuery() {
this._extension.endQuery();
this._queryStack.pop();
const activeQuery = this._queryStack[this._queryStack.length - 1];
this._extension.beginQuery(activeQuery.query);
}
}