📄 WebGLTimestampQueryPool.js
¶
📊 Analysis Summary¶
Metric | Count |
---|---|
🔧 Functions | 9 |
🧱 Classes | 1 |
📦 Imports | 2 |
📊 Variables & Constants | 7 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js
📦 Imports¶
Name | Source |
---|---|
warnOnce |
../../../utils.js |
TimestampQueryPool |
../../common/TimestampQueryPool.js |
Variables & Constants¶
Name | Type | Kind | Value | Exported |
---|---|---|---|---|
baseOffset |
number |
let/var | this.currentQueryIndex |
✗ |
query |
any |
let/var | this.queries[ baseOffset ] |
✗ |
resolvePromises |
any[] |
let/var | [] |
✗ |
query |
any |
let/var | this.queries[ baseOffset ] |
✗ |
results |
number[] |
let/var | await Promise.all( resolvePromises ) |
✗ |
timeoutId |
any |
let/var | *not shown* |
✗ |
isResolved |
boolean |
let/var | false |
✗ |
Functions¶
WebGLTimestampQueryPool.allocateQueriesForContext(renderContext: any): number
¶
JSDoc:
/**
* Allocates a pair of queries for a given render context.
*
* @param {Object} renderContext - The render context to allocate queries for.
* @returns {?number} The base offset for the allocated queries, or null if allocation failed.
*/
Parameters:
renderContext
any
Returns: number
Calls:
warnOnce (from ../../../utils.js)
this.type.toUpperCase
this.queryStates.set
this.queryOffsets.set
Internal Comments:
Code
allocateQueriesForContext( renderContext ) {
if ( ! this.trackTimestamp ) return null;
// Check if we have enough space for a new query pair
if ( this.currentQueryIndex + 2 > this.maxQueries ) {
warnOnce( `WebGPUTimestampQueryPool [${ this.type }]: Maximum number of queries exceeded, when using trackTimestamp it is necessary to resolves the queries via renderer.resolveTimestampsAsync( THREE.TimestampQuery.${ this.type.toUpperCase() } ).` );
return null;
}
const baseOffset = this.currentQueryIndex;
this.currentQueryIndex += 2;
// Initialize query states
this.queryStates.set( baseOffset, 'inactive' );
this.queryOffsets.set( renderContext.id, baseOffset );
return baseOffset;
}
WebGLTimestampQueryPool.beginQuery(renderContext: any): void
¶
JSDoc:
/**
* Begins a timestamp query for the specified render context.
*
* @param {Object} renderContext - The render context to begin timing for.
*/
Parameters:
renderContext
any
Returns: void
Calls:
this.queryOffsets.get
this.queryStates.get
this.gl.beginQuery
this.queryStates.set
console.error
Internal Comments:
Code
beginQuery( renderContext ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( renderContext.id );
if ( baseOffset == null ) {
return;
}
// Don't start a new query if there's an active one
if ( this.activeQuery !== null ) {
return;
}
const query = this.queries[ baseOffset ];
if ( ! query ) {
return;
}
try {
// Only begin if query is inactive
if ( this.queryStates.get( baseOffset ) === 'inactive' ) {
this.gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query );
this.activeQuery = baseOffset;
this.queryStates.set( baseOffset, 'started' );
}
} catch ( error ) {
console.error( 'Error in beginQuery:', error );
this.activeQuery = null;
this.queryStates.set( baseOffset, 'inactive' );
}
}
WebGLTimestampQueryPool.endQuery(renderContext: { id: string; }): void
¶
JSDoc:
/**
* Ends the active timestamp query for the specified render context.
*
* @param {Object} renderContext - The render context to end timing for.
* @param {string} renderContext.id - Unique identifier for the render context.
*/
Parameters:
renderContext
{ id: string; }
Returns: void
Calls:
this.queryOffsets.get
this.gl.endQuery
this.queryStates.set
console.error
Internal Comments:
Code
endQuery( renderContext ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( renderContext.id );
if ( baseOffset == null ) {
return;
}
// Only end if this is the active query
if ( this.activeQuery !== baseOffset ) {
return;
}
try {
this.gl.endQuery( this.ext.TIME_ELAPSED_EXT );
this.queryStates.set( baseOffset, 'ended' );
this.activeQuery = null;
} catch ( error ) {
console.error( 'Error in endQuery:', error );
// Reset state on error
this.queryStates.set( baseOffset, 'inactive' );
this.activeQuery = null;
}
}
WebGLTimestampQueryPool.resolveQueriesAsync(): Promise<number>
¶
JSDoc:
/**
* Asynchronously resolves all completed queries and returns the total duration.
*
* @async
* @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
*/
Returns: Promise<number>
Calls:
resolvePromises.push
this.resolveQuery
Promise.all
results.reduce
this.queryOffsets.clear
this.queryStates.clear
console.error
Internal Comments:
// Wait for all ended queries to complete (x2)
// Store the last valid result (x4)
// Reset states (x4)
Code
async resolveQueriesAsync() {
if ( ! this.trackTimestamp || this.pendingResolve ) {
return this.lastValue;
}
this.pendingResolve = true;
try {
// Wait for all ended queries to complete
const resolvePromises = [];
for ( const [ baseOffset, state ] of this.queryStates ) {
if ( state === 'ended' ) {
const query = this.queries[ baseOffset ];
resolvePromises.push( this.resolveQuery( query ) );
}
}
if ( resolvePromises.length === 0 ) {
return this.lastValue;
}
const results = await Promise.all( resolvePromises );
const totalDuration = results.reduce( ( acc, val ) => acc + val, 0 );
// Store the last valid result
this.lastValue = totalDuration;
// Reset states
this.currentQueryIndex = 0;
this.queryOffsets.clear();
this.queryStates.clear();
this.activeQuery = null;
return totalDuration;
} catch ( error ) {
console.error( 'Error resolving queries:', error );
return this.lastValue;
} finally {
this.pendingResolve = false;
}
}
WebGLTimestampQueryPool.resolveQuery(query: WebGLQuery): Promise<number>
¶
JSDoc:
/**
* Resolves a single query, checking for completion and disjoint operation.
*
* @async
* @param {WebGLQuery} query - The query object to resolve.
* @returns {Promise<number>} The elapsed time in milliseconds.
*/
Parameters:
query
WebGLQuery
Returns: Promise<number>
Calls:
resolve
clearTimeout
cleanup
finalizeResolution
this.gl.getParameter
this.gl.getQueryParameter
setTimeout
Number
console.error
checkQuery
Internal Comments:
Code
async resolveQuery( query ) {
return new Promise( ( resolve ) => {
if ( this.isDisposed ) {
resolve( this.lastValue );
return;
}
let timeoutId;
let isResolved = false;
const cleanup = () => {
if ( timeoutId ) {
clearTimeout( timeoutId );
timeoutId = null;
}
};
const finalizeResolution = ( value ) => {
if ( ! isResolved ) {
isResolved = true;
cleanup();
resolve( value );
}
};
const checkQuery = () => {
if ( this.isDisposed ) {
finalizeResolution( this.lastValue );
return;
}
try {
// Check if the GPU timer was disjoint (i.e., timing was unreliable)
const disjoint = this.gl.getParameter( this.ext.GPU_DISJOINT_EXT );
if ( disjoint ) {
finalizeResolution( this.lastValue );
return;
}
const available = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT_AVAILABLE );
if ( ! available ) {
timeoutId = setTimeout( checkQuery, 1 );
return;
}
const elapsed = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT );
resolve( Number( elapsed ) / 1e6 ); // Convert nanoseconds to milliseconds
} catch ( error ) {
console.error( 'Error checking query:', error );
resolve( this.lastValue );
}
};
checkQuery();
} );
}
WebGLTimestampQueryPool.dispose(): void
¶
JSDoc:
/**
* Releases all resources held by this query pool.
* This includes deleting all query objects and clearing internal state.
*/
Returns: void
Calls:
this.gl.deleteQuery
this.queryStates.clear
this.queryOffsets.clear
Code
cleanup(): void
¶
Returns: void
Calls:
clearTimeout
finalizeResolution(value: any): void
¶
Parameters:
value
any
Returns: void
Calls:
cleanup
resolve
checkQuery(): void
¶
Returns: void
Calls:
finalizeResolution
this.gl.getParameter
this.gl.getQueryParameter
setTimeout
resolve
Number
console.error
Internal Comments:
Code
() => {
if ( this.isDisposed ) {
finalizeResolution( this.lastValue );
return;
}
try {
// Check if the GPU timer was disjoint (i.e., timing was unreliable)
const disjoint = this.gl.getParameter( this.ext.GPU_DISJOINT_EXT );
if ( disjoint ) {
finalizeResolution( this.lastValue );
return;
}
const available = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT_AVAILABLE );
if ( ! available ) {
timeoutId = setTimeout( checkQuery, 1 );
return;
}
const elapsed = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT );
resolve( Number( elapsed ) / 1e6 ); // Convert nanoseconds to milliseconds
} catch ( error ) {
console.error( 'Error checking query:', error );
resolve( this.lastValue );
}
}
Classes¶
WebGLTimestampQueryPool
¶
Class Code
class WebGLTimestampQueryPool extends TimestampQueryPool {
/**
* Creates a new WebGL timestamp query pool.
*
* @param {WebGLRenderingContext|WebGL2RenderingContext} gl - The WebGL context.
* @param {string} type - The type identifier for this query pool.
* @param {number} [maxQueries=2048] - Maximum number of queries this pool can hold.
*/
constructor( gl, type, maxQueries = 2048 ) {
super( maxQueries );
this.gl = gl;
this.type = type;
// Check for timer query extensions
this.ext = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' ) ||
gl.getExtension( 'EXT_disjoint_timer_query' );
if ( ! this.ext ) {
console.warn( 'EXT_disjoint_timer_query not supported; timestamps will be disabled.' );
this.trackTimestamp = false;
return;
}
// Create query objects
this.queries = [];
for ( let i = 0; i < this.maxQueries; i ++ ) {
this.queries.push( gl.createQuery() );
}
this.activeQuery = null;
this.queryStates = new Map(); // Track state of each query: 'inactive', 'started', 'ended'
}
/**
* Allocates a pair of queries for a given render context.
*
* @param {Object} renderContext - The render context to allocate queries for.
* @returns {?number} The base offset for the allocated queries, or null if allocation failed.
*/
allocateQueriesForContext( renderContext ) {
if ( ! this.trackTimestamp ) return null;
// Check if we have enough space for a new query pair
if ( this.currentQueryIndex + 2 > this.maxQueries ) {
warnOnce( `WebGPUTimestampQueryPool [${ this.type }]: Maximum number of queries exceeded, when using trackTimestamp it is necessary to resolves the queries via renderer.resolveTimestampsAsync( THREE.TimestampQuery.${ this.type.toUpperCase() } ).` );
return null;
}
const baseOffset = this.currentQueryIndex;
this.currentQueryIndex += 2;
// Initialize query states
this.queryStates.set( baseOffset, 'inactive' );
this.queryOffsets.set( renderContext.id, baseOffset );
return baseOffset;
}
/**
* Begins a timestamp query for the specified render context.
*
* @param {Object} renderContext - The render context to begin timing for.
*/
beginQuery( renderContext ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( renderContext.id );
if ( baseOffset == null ) {
return;
}
// Don't start a new query if there's an active one
if ( this.activeQuery !== null ) {
return;
}
const query = this.queries[ baseOffset ];
if ( ! query ) {
return;
}
try {
// Only begin if query is inactive
if ( this.queryStates.get( baseOffset ) === 'inactive' ) {
this.gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query );
this.activeQuery = baseOffset;
this.queryStates.set( baseOffset, 'started' );
}
} catch ( error ) {
console.error( 'Error in beginQuery:', error );
this.activeQuery = null;
this.queryStates.set( baseOffset, 'inactive' );
}
}
/**
* Ends the active timestamp query for the specified render context.
*
* @param {Object} renderContext - The render context to end timing for.
* @param {string} renderContext.id - Unique identifier for the render context.
*/
endQuery( renderContext ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( renderContext.id );
if ( baseOffset == null ) {
return;
}
// Only end if this is the active query
if ( this.activeQuery !== baseOffset ) {
return;
}
try {
this.gl.endQuery( this.ext.TIME_ELAPSED_EXT );
this.queryStates.set( baseOffset, 'ended' );
this.activeQuery = null;
} catch ( error ) {
console.error( 'Error in endQuery:', error );
// Reset state on error
this.queryStates.set( baseOffset, 'inactive' );
this.activeQuery = null;
}
}
/**
* Asynchronously resolves all completed queries and returns the total duration.
*
* @async
* @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
*/
async resolveQueriesAsync() {
if ( ! this.trackTimestamp || this.pendingResolve ) {
return this.lastValue;
}
this.pendingResolve = true;
try {
// Wait for all ended queries to complete
const resolvePromises = [];
for ( const [ baseOffset, state ] of this.queryStates ) {
if ( state === 'ended' ) {
const query = this.queries[ baseOffset ];
resolvePromises.push( this.resolveQuery( query ) );
}
}
if ( resolvePromises.length === 0 ) {
return this.lastValue;
}
const results = await Promise.all( resolvePromises );
const totalDuration = results.reduce( ( acc, val ) => acc + val, 0 );
// Store the last valid result
this.lastValue = totalDuration;
// Reset states
this.currentQueryIndex = 0;
this.queryOffsets.clear();
this.queryStates.clear();
this.activeQuery = null;
return totalDuration;
} catch ( error ) {
console.error( 'Error resolving queries:', error );
return this.lastValue;
} finally {
this.pendingResolve = false;
}
}
/**
* Resolves a single query, checking for completion and disjoint operation.
*
* @async
* @param {WebGLQuery} query - The query object to resolve.
* @returns {Promise<number>} The elapsed time in milliseconds.
*/
async resolveQuery( query ) {
return new Promise( ( resolve ) => {
if ( this.isDisposed ) {
resolve( this.lastValue );
return;
}
let timeoutId;
let isResolved = false;
const cleanup = () => {
if ( timeoutId ) {
clearTimeout( timeoutId );
timeoutId = null;
}
};
const finalizeResolution = ( value ) => {
if ( ! isResolved ) {
isResolved = true;
cleanup();
resolve( value );
}
};
const checkQuery = () => {
if ( this.isDisposed ) {
finalizeResolution( this.lastValue );
return;
}
try {
// Check if the GPU timer was disjoint (i.e., timing was unreliable)
const disjoint = this.gl.getParameter( this.ext.GPU_DISJOINT_EXT );
if ( disjoint ) {
finalizeResolution( this.lastValue );
return;
}
const available = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT_AVAILABLE );
if ( ! available ) {
timeoutId = setTimeout( checkQuery, 1 );
return;
}
const elapsed = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT );
resolve( Number( elapsed ) / 1e6 ); // Convert nanoseconds to milliseconds
} catch ( error ) {
console.error( 'Error checking query:', error );
resolve( this.lastValue );
}
};
checkQuery();
} );
}
/**
* Releases all resources held by this query pool.
* This includes deleting all query objects and clearing internal state.
*/
dispose() {
if ( this.isDisposed ) {
return;
}
this.isDisposed = true;
if ( ! this.trackTimestamp ) return;
for ( const query of this.queries ) {
this.gl.deleteQuery( query );
}
this.queries = [];
this.queryStates.clear();
this.queryOffsets.clear();
this.lastValue = 0;
this.activeQuery = null;
}
}
Methods¶
allocateQueriesForContext(renderContext: any): number
¶
Code
allocateQueriesForContext( renderContext ) {
if ( ! this.trackTimestamp ) return null;
// Check if we have enough space for a new query pair
if ( this.currentQueryIndex + 2 > this.maxQueries ) {
warnOnce( `WebGPUTimestampQueryPool [${ this.type }]: Maximum number of queries exceeded, when using trackTimestamp it is necessary to resolves the queries via renderer.resolveTimestampsAsync( THREE.TimestampQuery.${ this.type.toUpperCase() } ).` );
return null;
}
const baseOffset = this.currentQueryIndex;
this.currentQueryIndex += 2;
// Initialize query states
this.queryStates.set( baseOffset, 'inactive' );
this.queryOffsets.set( renderContext.id, baseOffset );
return baseOffset;
}
beginQuery(renderContext: any): void
¶
Code
beginQuery( renderContext ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( renderContext.id );
if ( baseOffset == null ) {
return;
}
// Don't start a new query if there's an active one
if ( this.activeQuery !== null ) {
return;
}
const query = this.queries[ baseOffset ];
if ( ! query ) {
return;
}
try {
// Only begin if query is inactive
if ( this.queryStates.get( baseOffset ) === 'inactive' ) {
this.gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query );
this.activeQuery = baseOffset;
this.queryStates.set( baseOffset, 'started' );
}
} catch ( error ) {
console.error( 'Error in beginQuery:', error );
this.activeQuery = null;
this.queryStates.set( baseOffset, 'inactive' );
}
}
endQuery(renderContext: { id: string; }): void
¶
Code
endQuery( renderContext ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( renderContext.id );
if ( baseOffset == null ) {
return;
}
// Only end if this is the active query
if ( this.activeQuery !== baseOffset ) {
return;
}
try {
this.gl.endQuery( this.ext.TIME_ELAPSED_EXT );
this.queryStates.set( baseOffset, 'ended' );
this.activeQuery = null;
} catch ( error ) {
console.error( 'Error in endQuery:', error );
// Reset state on error
this.queryStates.set( baseOffset, 'inactive' );
this.activeQuery = null;
}
}
resolveQueriesAsync(): Promise<number>
¶
Code
async resolveQueriesAsync() {
if ( ! this.trackTimestamp || this.pendingResolve ) {
return this.lastValue;
}
this.pendingResolve = true;
try {
// Wait for all ended queries to complete
const resolvePromises = [];
for ( const [ baseOffset, state ] of this.queryStates ) {
if ( state === 'ended' ) {
const query = this.queries[ baseOffset ];
resolvePromises.push( this.resolveQuery( query ) );
}
}
if ( resolvePromises.length === 0 ) {
return this.lastValue;
}
const results = await Promise.all( resolvePromises );
const totalDuration = results.reduce( ( acc, val ) => acc + val, 0 );
// Store the last valid result
this.lastValue = totalDuration;
// Reset states
this.currentQueryIndex = 0;
this.queryOffsets.clear();
this.queryStates.clear();
this.activeQuery = null;
return totalDuration;
} catch ( error ) {
console.error( 'Error resolving queries:', error );
return this.lastValue;
} finally {
this.pendingResolve = false;
}
}
resolveQuery(query: WebGLQuery): Promise<number>
¶
Code
async resolveQuery( query ) {
return new Promise( ( resolve ) => {
if ( this.isDisposed ) {
resolve( this.lastValue );
return;
}
let timeoutId;
let isResolved = false;
const cleanup = () => {
if ( timeoutId ) {
clearTimeout( timeoutId );
timeoutId = null;
}
};
const finalizeResolution = ( value ) => {
if ( ! isResolved ) {
isResolved = true;
cleanup();
resolve( value );
}
};
const checkQuery = () => {
if ( this.isDisposed ) {
finalizeResolution( this.lastValue );
return;
}
try {
// Check if the GPU timer was disjoint (i.e., timing was unreliable)
const disjoint = this.gl.getParameter( this.ext.GPU_DISJOINT_EXT );
if ( disjoint ) {
finalizeResolution( this.lastValue );
return;
}
const available = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT_AVAILABLE );
if ( ! available ) {
timeoutId = setTimeout( checkQuery, 1 );
return;
}
const elapsed = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT );
resolve( Number( elapsed ) / 1e6 ); // Convert nanoseconds to milliseconds
} catch ( error ) {
console.error( 'Error checking query:', error );
resolve( this.lastValue );
}
};
checkQuery();
} );
}