拓冰建站拓冰建站
首页 / 资讯中心 / 正文

three.js WebGL 后端 GPU 时间戳查询池(WebGLTimestampQueryPool)深度解析

three.js WebGL 后端 GPU 时间戳查询池WebGLTimestampQueryPool深度解析【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsWebGLTimestampQueryPool是 three.js 在 WebGL 渲染路径下webgl-fallback 后端用于 GPU 耗时测量的时间戳查询池。它负责按上下文成对分配查询对象并通过EXT_disjoint_timer_query_webgl2/EXT_disjoint_timer_query扩展完成查询的创建、执行与异步结果回收。读完本文你将掌握该查询池从分配、开始/结束到异步 resolve 的完整状态机与调用链理解 render/compute 两类耗时GPU 时间戳如何在 three.js 中采集并能直接利用 Renderer 的info接口读取每帧渲染与计算耗时。本文基于 three.js 仓库中的 官方 API 文档 与同名源码WebGLTimestampQueryPool.js整理从上层 API 一直追到底层 WebGL 调用并补充其基类与 Backend 级调用链作为佐证。一、类定位继承关系与职责从文档首行标注的继承信息可以看出Inheritance: TimestampQueryPool → WebGLTimestampQueryPoolWebGLTimestampQueryPool继承自渲染器公共层的抽象基类 TimestampQueryPool。基类负责维护与具体 GPU API 无关的通用状态trackTimestamp默认true是否开启时间戳追踪maxQueries基类默认256查询池容量currentQueryIndex已分配查询的游标queryOffsetsMapstring, number渲染上下文 uid → 查询偏移量timestampsMapstring, numberuid → 解析出的毫秒耗时frames/lastValue/pendingResolve/isDisposed等通用状态。同时基类把三个关键方法声明为抽象方法由各 GPU 后端各自实现抽象方法说明allocateQueriesForContext( uid )为某 uid 分配查询resolveQueriesAsync()异步解析所有时间戳dispose()释放查询池在 three.js 中WebGLwebgl-fallback与 WebGPUWebGPUTimestampQueryPool各有一个实现WebGPU 版基于GPUQuerySet 查询缓冲区而 WebGL 版基于本文的 WebGL 查询对象与定时器扩展。二者共享同一套 uid/帧统计语义便于上层如 Inspector、renderer.info无差别消费耗时数据。WebGLTimestampQueryPool的核心职责可概括为一条文档原话Manages a pool of WebGL timestamp queries for performance measurement. Handles creation, execution, and resolution of timer queries using WebGL extensions.管理一批用于性能测量的 WebGL 时间戳查询负责借助 WebGL 扩展完成计时查询的创建、执行与结果解析。二、构造函数与查询对象的预分配构造签名new WebGLTimestampQueryPool( gl : WebGLRenderingContext | WebGL2RenderingContext, type : string, maxQueries : number )参数类型说明glWebGLRenderingContext \| WebGL2RenderingContextWebGL 上下文对象typestring该查询池的类型标识渲染/计算等分类maxQueriesnumber查询池可容纳的最大查询数量默认2048对应源码实现WebGLTimestampQueryPool.jsconstructor( 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 ) { 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(); // inactive, started, ended }扩展探测的优先级构造器按优先级先后探测两个 WebGL 扩展EXT_disjoint_timer_query_webgl2面向 WebGL2基于gl.beginQuery/endQuery/QUERY_RESULT等标准 APIEXT_disjoint_timer_query面向 WebGL1 的旧版扩展。若两者都不存在代码会发出warnEXT_disjoint_timer_query not supported; timestamps will be disabled.并把继承自基类的trackTimestamp置为false。此后所有公共方法都会在入口处因!this.trackTimestamp提前返回整体自动退化为不追踪计时而非抛错——这是查询池健壮性设计的关键一环。查询对象池化为规避每帧创建/销毁 GPU 查询对象的开销构造器在gl.createQuery()上一次预分配满maxQueries默认 2048个WebGLQuery对象存入this.queries配合三个内部状态字段协同工作queries[]物理查询对象池activeQuery当前正在运行started 未 ended的查询偏移量null表示空闲queryStatesMapnumber, string每个偏移量所属状态取值inactive、started、ended构成严格的单查询状态机。type 字段的来源从 WebGLBackend.initTimestampQuery 可见池是按 type 惰性创建的创建时固定传入2048源码留有// TODO: Variable maxQueries?注释if ( ! this.timestampQueryPool[ type ] ) { this.timestampQueryPool[ type ] new WebGLTimestampQueryPool( this.gl, type, 2048 ); }结合 Backend 公共层 Backend.js 中uid.startsWith(c:) ? TimestampQuery.COMPUTE : TimestampQuery.RENDER的判定逻辑可推断type在实际运行中对应render与compute两类计时分别用于渲染与计算Compute Node流水线的耗时测量。三、查询的分配与生命周期allocate / begin / end.allocateQueriesForContext( uid : string ) : number签名allocateQueriesForContext( uid )返回分配得到的查询基偏移量base offset分配失败返回null。覆盖TimestampQueryPool#allocateQueriesForContext基类为空实现WebGL 子类给出实体逻辑为某个渲染上下文uid 唯一标识分配一对查询对象偏移量为baseOffset与baseOffset 1后续可用baseOffset连续成对消费槽位。完整实现源码allocateQueriesForContext( uid ) { if ( ! this.trackTimestamp ) return null; if ( this.currentQueryIndex 2 this.maxQueries ) { this.resolveQueriesAsync(); this.currentQueryIndex 0; this.queryOffsets.clear(); this.queryStates.clear(); this.activeQuery null; } const baseOffset this.currentQueryIndex; this.currentQueryIndex 2; this.queryStates.set( baseOffset, inactive ); this.queryOffsets.set( uid, baseOffset ); return baseOffset; }要点解读池满自动回绕当currentQueryIndex 2 maxQueries即池被耗尽时会先异步调用resolveQueriesAsync()回收已结束查询的结果然后把游标清零、清空全部映射与状态重新开始循环利用槽位未手动调await回绕期数据是否可解析取决于异步时序属实现细节。uid → offset 登记queryOffsets建立 uid 与基偏移的映射供后续beginQuery/endQuery查找。每次调用消耗2 个槽位成对语义而 begin/end 实际只使用baseOffset一个查询对象计时预留的成对结构是与 WebGPU 后端保持接口一致性的设计。.beginQuery( uid : string )开始一次针对指定渲染上下文的时间戳查询源码beginQuery( uid ) { if ( ! this.trackTimestamp || this.isDisposed ) return; const baseOffset this.queryOffsets.get( uid ); if ( baseOffset null ) return; // Dont start a new query if theres an active one if ( this.activeQuery ! null ) return; const query this.queries[ baseOffset ]; if ( ! query ) return; try { if ( this.queryStates.get( baseOffset ) inactive ) { this.gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query ); this.activeQuery baseOffset; this.queryStates.set( baseOffset, started ); } } catch ( e ) { error( Error in beginQuery:, e ); this.activeQuery null; this.queryStates.set( baseOffset, inactive ); } }关键逻辑互斥保护activeQuery ! null时直接返回杜绝嵌套 begin保证任意时刻 GPU 上只有一个活动的计时查询。状态门控仅当该偏移处于inactive时才调用gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query )随后置为started并记录activeQuery。这里使用的目标是TIME_ELAPSED_EXT测量命令实际消耗的 GPU 时间而非TIMESTAMP_EXT瞬间时钟快照。异常兜底gl.beginQuery抛错时记录error并复位状态避免状态机卡死。.endQuery( uid : string )结束指定上下文的活动查询源码endQuery( uid ) { if ( ! this.trackTimestamp || this.isDisposed ) return; const baseOffset this.queryOffsets.get( uid ); 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 ( e ) { error( Error in endQuery:, e ); this.queryStates.set( baseOffset, inactive ); this.activeQuery null; } }与 beginQuery 对称只有当前活动查询确实是本 uid时才调用gl.endQuery成功后状态推进到ended只有ended的查询才会进入后续异步解析阶段失败则回退为inactive。调用链render 与 compute 两条流水线在 WebGLBackend 中begin/end 由后端按渲染生命周期自动触发渲染路径initTimestampQuery( TimestampQuery.RENDER, ...) 在渲染前调用 → 内部执行allocateQueriesForContextbeginQuery随后 prepareTimestampBuffer( TimestampQuery.RENDER, ...) 在渲染后调用 → 执行endQuery。计算路径Compute Node 执行前后分别触发 initTimestampQuery( TimestampQuery.COMPUTE, ...) 与 prepareTimestampBuffer( TimestampQuery.COMPUTE, ...)。其中后端只会在EXT_disjoint_timer_query_webgl2扩展可用this.disjoint ! null见 hasTimestamp且trackTimestamp开启时才真正分配查询。整体时序可概括为render/compute 开始 → initTimestampQuery(type, uid) → 池满则回绕 → allocateQueriesForContext(uid) → beginQuery(uid) [gl.beginQuery] render/compute 结束 → prepareTimestampBuffer(type, uid) → endQuery(uid) [gl.endQuery]四、异步结果解析resolveQueriesAsync 与帧统计.resolveQueriesAsync() : Promise.异步解析所有已结束的查询并返回耗时总和毫秒解析失败时回退为最近一次有效值lastValue。覆盖TimestampQueryPool#resolveQueriesAsync。完整实现源码核心分四步批量收集ended查询遍历queryOffsets凡状态为ended的偏移取出物理WebGLQuery交给resolveQuery(query)生成 Promise以 uid 为键存入resolvePromises。按帧聚合耗时uid 约定形如xxx:f帧号代码用/^(.*):f(\d)$/正则切出帧号将属于同一帧的各查询耗时累加进framesDuration[frame]同时逐条写入this.timestamps.set(uid, duration)供上层按 uid 取数。返回末帧总耗时totalDuration framesDuration[ 最后一帧 ]并写入this.lastValue与this.frames。整体复位清空游标与queryOffsets/queryStates/activeQuery为下一轮循环复用做准备。async resolveQueriesAsync() { if ( ! this.trackTimestamp || this.pendingResolve ) { return this.lastValue; // 防重入 } this.pendingResolve true; try { const resolvePromises new Map(); for ( const [ uid, baseOffset ] of this.queryOffsets ) { if ( this.queryStates.get( baseOffset ) ended ) { resolvePromises.set( uid, this.resolveQuery( this.queries[ baseOffset ] ) ); } } if ( resolvePromises.size 0 ) return this.lastValue; const framesDuration {}; const frames []; this.timestamps.clear(); for ( const [ uid, promise ] of resolvePromises ) { const match uid.match( /^(.*):f(\d)$/ ); const frame parseInt( match[ 2 ] ); if ( frames.includes( frame ) false ) frames.push( frame ); if ( framesDuration[ frame ] undefined ) framesDuration[ frame ] 0; const duration await promise; this.timestamps.set( uid, duration ); framesDuration[ frame ] duration; } const totalDuration framesDuration[ frames[ frames.length - 1 ] ]; this.lastValue totalDuration; this.frames frames; // Reset states this.currentQueryIndex 0; this.queryOffsets.clear(); this.queryStates.clear(); this.activeQuery null; return totalDuration; } catch ( e ) { error( Error resolving queries:, e ); return this.lastValue; // 出错时回退旧值 } finally { this.pendingResolve false; } }防重入标志pendingResolve基类注释指出 WebGPU 后端将其用作 Promise 本体WebGL 后端仅当布尔开关确保不会并发多轮 resolve。.resolveQuery( query : WebGLQuery ) : Promise.解析单个查询逐项检查 GPU 定时器是否 disjoint、结果是否可读源码async resolveQuery( query ) { return new Promise( ( resolve ) { if ( this.isDisposed ) { resolve( this.lastValue ); return; } let timeoutId; let isResolved false; const finalizeResolution ( value ) { if ( ! isResolved ) { isResolved true; if ( timeoutId ) { clearTimeout( timeoutId ); timeoutId null; } resolve( value ); } }; const checkQuery () { if ( this.isDisposed ) { finalizeResolution( this.lastValue ); return; } try { // GPU 定时器被 disjoint计时结果不可信 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 ) { // 结果尚不可用 → 1ms 后轮询 timeoutId setTimeout( checkQuery, 1 ); return; } const elapsed this.gl.getQueryParameter( query, this.gl.QUERY_RESULT ); resolve( Number( elapsed ) / 1e6 ); // 纳秒 → 毫秒 } catch ( e ) { error( Error checking query:, e ); resolve( this.lastValue ); } }; checkQuery(); } ); }解析细节说明disjoint不连续检测GPU_DISJOINT_EXT为真表示 GPU 频率/状态发生变化如切换省电模式计时结果不可信此时按文档语义返回最近一次有效值处理防止把脏数据写入统计。轮询策略结果未就绪时以setTimeout(checkQuery, 1)每毫秒自旋轮询QUERY_RESULT_AVAILABLE直到可读通过finalizeResolutionisResolved保证每个查询只 resolve 一次并正确清理定时器。单位换算QUERY_RESULT返回纳秒整数Number(elapsed) / 1e6换算为毫秒。.dispose()释放查询池持有的全部资源源码。覆盖TimestampQueryPool#dispose。幂等实现——已 dispose 直接返回随后删除所有WebGLQuery对象gl.deleteQuery清空queries数组与queryStates/queryOffsets/timestamps/frames/lastValue/activeQuery等全部内部状态。销毁后的池内任何 resolve 都会安全回退到lastValue。五、上层消费耗时如何到达 renderer.info从查询池到开发者可见指标需要经过 Backend 公共层 的桥接getTimestampUID( abstractRenderContext )为每次 render/compute 生成含帧号的 uidgetTimestampFrames( type )返回查询池记录的帧号数组getTimestamp( uid ) / hasTimestampQuery( uid )按 uid 取单个耗时/判断可用性如搭配renderer.info判定该帧是否存在有效的 GPU 时间戳resolveTimestampsAsync( type render )调用对应池的resolveQueriesAsync()并把返回值写入this.renderer.info[ type ].timestamp。因此当渲染器开启了时间戳追踪后开发者可以这样读取 GPU 耗时此处以典型使用流程示意详细 API 以 Renderer 与 WebGLBackend 源码为准// 渲染若干帧后在合适时机解析时间戳 await renderer.backend.resolveTimestampsAsync( render ); // 读取上一轮解析得到的渲染 GPU 耗时毫秒 const gpuRenderTime renderer.info.render.timestamp;配合 Inspector 体系Renderer.js 在 render/compute 前后调用的beginRender/beginCompute/finishRender/finishComputeWebGLTimestampQueryPool得以在 three.js 官方调试面板与性能分析工具中输出逐帧 GPU 耗时数据。六、适用前提与限制综合文档与源码使用本查询池有几个前提与边界需要明确扩展依赖必须存在EXT_disjoint_timer_query_webgl2WebGL2或EXT_disjoint_timer_queryWebGL1。不可用时 timestamps 自动关闭不会抛错但getTimestamp查询不到数据。后端分支本文描述的是 WebGLwebgl-fallback路径若使用 WebGPU 渲染器对应实现是 WebGPUTimestampQueryPool其基于 WebGPU 的Timestamp-Query特性与查询缓冲区使用语义uid、帧聚合、lastValue回退保持一致但底层完全不同。容量与回绕默认maxQueries 2048构造函数与 WebGLBackend 中创建池时传入的2048一致池满后自动触发异步 resolve 并回绕游标因此超长连续采样的帧数据可能因回绕被覆盖。数据可靠性一旦 GPU 报告 disjoint省电/频率切换等该批结果会被丢弃并回退lastValue结果未就绪时采用 1ms 轮询解析为异步操作不应在渲染主循环同步关键路径上阻塞等待。七、小结WebGLTimestampQueryPool通过预分配查询对象池 inactive/started/ended 状态机 async 轮询解析 按帧聚合统计四层设计把 WebGL 定时器扩展封装成 three.js 统一的 GPU 耗时测量设施allocateQueriesForContext负责按 uid 分配槽位beginQuery/endQuery控制 GPU 计时区间resolveQueriesAsync/resolveQuery处理 disjoint 检测、可用性轮询与纳秒到毫秒的换算dispose负责资源回收。理解这条从扩展探测到renderer.info[type].timestamp的完整链路即可在 WebGL 后端下自行接入或分析 GPU 渲染/计算耗时的采样数据。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门