返回知识库

GIS

调优手册

调优手册 封面
GISengine-webgpu性能调优排错

前情回顾04 影像图层 讲清了多层影像混合与纹理管理,05 地形与 Worker 讲清了 RGB/BIL 高程编码与异步管线加载,08 渲染管线 深入 GPU 五阶段、Tone Mapping 与大气散射。当引擎遇到卡顿、内存涨、瓦片不加载时,怎么定位问题?资源什么时候释放?缓存满了怎么办?本篇回答:性能瓶颈排查与资源调优。

直觉问题

打开一个运行了一段时间的 3D 地图应用,三个最常见的困扰:

  • 引擎跑了一段时间后 FPS 从 60 掉到 20——不是突然变慢的,是 gradually degrading。哪里出问题了?CPU 算不过来了?GPU draw call 太多了?内存泄漏了?
  • 飞过太平洋又飞回来,瓦片会重新加载吗? ——之前的瓦片纹理会一直占着内存不释放吗?如果用户一直漫游,内存会不会无限制增长?
  • 地图加载到一半,某个区域的瓦片永远空白——是网络超时没重试?是剔除算法把偏远瓦片误判为不可见?还是瓦片解码崩溃了?

读完本篇,你能回答:“如何区分 CPU-bound 和 GPU-bound 的性能瓶颈?""性能优化的第一原则是什么?""LRU 缓存的淘汰逻辑是什么?“

核心概念白话讲

用”水管阻塞”理解 GPU 性能瓶颈

  • CPU-bound:CPU 算得太慢,GPU 空闲 → 优化:减少 JS 计算、优化数据结构
  • GPU-bound:GPU 太忙,CPU 等 GPU → 优化:减少 draw call、简化 shader
  • Memory-bound:内存带宽成为瓶颈 → 优化:减少纹理采样、压缩纹理
  • Network-bound:网络下载太慢 → 优化:减少并发、启用缓存
性能瓶颈排查决策树

用”酒店房间”理解 LRU 缓存

  • 每个瓦片纹理 = 一个房间
  • 缓存大小 = 酒店总房间数
  • LRU (Least Recently Used):满房时,清退最久未访问的房客
  • 核心假设:时空局部性——最近访问的更可能再次访问
LRU 缓存淘汰示意

用”航班时刻表”理解帧预算管理

60 FPS = 每帧 16.67ms:

阶段预算超出时优化
JS 逻辑~5ms主线程卡死拆长任务、放 Worker
渲染提交~3msdraw call 瓶颈实例化、批处理
GPU 渲染~6ms画面无法刷新简化 shader
浏览器合成~2ms浏览器响应慢减少 DOM 操作
缓冲~0.67ms应对突发留足 headroom

NOTE

关键:60 FPS 的目标是所有阶段总和不超过 16.67ms。JS 逻辑时间经常被忽略,但它经常超过 GPU 渲染时间。

原理与数学/机制

1. 帧预算分配与瓶颈定位

60 FPS 等价于每帧 16.67ms。这 16.67ms 不是”GPU 专用时间”,而是 CPU 调度、JS 执行、渲染命令提交、GPU 绘制、浏览器合成五阶段的总和。我们把它们放进一个”航班时刻表”:

60 FPS 帧预算饼图

当总耗时超过 16.67ms 时,浏览器只能以 30 FPS(33.33ms/帧)呈现,用户会明显感知卡顿。性能优化的第一原则不是”把每个阶段都优化到极致”,而是”找到最窄的瓶颈并优先扩展它”——就像疏通水管的堵塞点,而不是把所有水管都换成钻石材质。

帧预算分配不是固定的。在简单场景(仅底图、无地形、无 3D 建筑)中,JS 逻辑可能只消耗 1ms,GPU 渲染也只需 2ms,此时你的性能余量高达 13ms。但在复杂场景(地形 + 倾斜摄影 + 实时大气 + 大规模矢量标绘)中,任何一个阶段都可能突破预算。

IMPORTANT

性能分析不是先优化,而是先测量。没有 DevTools 数据支撑的优化,就像在黑暗房间里找黑猫。

2. 性能指标阈值速查

指标健康阈值告警阈值危险阈值
FPS≥ 5530–55< 30
JS 耗时/帧< 5ms5–8ms> 10ms
Draw Calls/帧< 200200–500> 800
GPU 顶点数/帧< 1M1M–3M> 5M
纹理上传/帧< 2MB2–5MB> 8MB
并发网络请求< 66–10> 15

这张表的价值在于”先看数据再动手”。当用户报告”卡”时,先用 DevTools 确认是哪一列超标,再针对性优化。

3. LRU 缓存数学与时空局部性

LRU(Least Recently Used)是 WebGIS 瓦片缓存中最经典的淘汰策略。它的数学基础是时空局部性原理:最近被访问的数据,在未来也极有可能被再次访问。

假设缓存容量,LOCATION,PLACE,CONCEPT,ENGINEERING capacity 为 CC,访问序列为 a1,a2,...,ana_1, a_2, ..., a_n命中率定义为:

HitRate=AccessesMissesAccesses=1MissesAccessesHitRate = \frac{Accesses - Misses}{Accesses} = 1 - \frac{Misses}{Accesses}

对于瓦片缓存而言,一次”Miss”意味着需要从网络下载一张 256×256 像素的 PNG/JPEG(通常 20KB–100KB),或者从磁盘/内存中读取解码后的 RGBA 纹理数据。在网络 I/O 成为瓶颈的场景下,缓存命中率每提升 10%,用户感知的加载延迟就能降低一半以上。

LRU 的平均时间复杂度

  • 读取/写入缓存:O(1)O(1)(使用哈希表 + 双向链表实现)
  • 淘汰最久未访问项:O(1)O(1)

NOTE

为什么不用 LFU? LFU(Least Frequently Used)淘汰访问频率最低的项。在地图漫游场景中,用户可能突然放大到某个新区域,此时 LFU 会错误地保留旧区域的高频瓦片,反而降低命中率。LRU 对”突发访问模式”更鲁棒。

4. 两阶段清理与 GPU 内存管理

WebGL 资源(纹理、Buffer、FBO、Shader Program)的生命周期由 JavaScript 对象引用和 GPU 驱动两层共同管理。JS 层通过 gl.deleteTexture() 向 GPU 驱动发信号”可以释放了”,但真正的显存回收由 GPU 驱动的垃圾回收器在帧间隙异步执行。

如果材质和纹理之间互相引用,一次性 dispose 会导致:

  1. 纹理被释放,但材质仍持有野指针
  2. 下一帧渲染时,材质尝试绑定一个已被删除的纹理 → WebGL CONTEXT_LOST

因此必须采用两阶段清理

资源清理两阶段流程

IMPORTANT

为什么必须等一帧? 因为材质和纹理之间、纹理和 FBO 之间、FBO 和渲染目标之间可能存在多层引用关系。延迟一帧确保 WebGL 的 delete 命令已发送到 GPU 驱动,且上一帧的渲染命令已执行完毕,避免”使用中释放”的竞态条件。

5. GPU 显存占用估算

一张瓦片纹理的 GPU 内存占用取决于其尺寸、格式和 Mipmapping:

Memorytexture=Width×Height×BytesPerPixel×(1+MipmapOverhead)Memory_{texture} = Width \times Height \times BytesPerPixel \times (1 + MipmapOverhead)

其中 MipmapOverhead ≈ 1.33(因为 Mipmap 链总和是原图的 1 + 1/4 + 1/16 + … ≈ 1.33 倍)。

以一张 256×256 的 RGBA 瓦片为例:

格式Bytes/Pixel单张内存1000 张缓存
RGBA8 (未压缩)4256 KB~250 MB
DXT1 / BC10.532 KB~32 MB
ETC20.532 KB~32 MB
ASTC 4×4164 KB~64 MB

TIP

经验法则:移动设备 GPU 显存通常在 1–4GB 之间,其中浏览器可能被限额到 512MB–1GB。如果你的缓存策略允许 2000 张未压缩的 256×256 RGBA 瓦片,就已经突破了上限。启用纹理压缩(如 KTX2 / ETC2)可以将内存占用降低 4–8 倍。

6. Draw Call 优化与渲染状态排序

方案Draw Calls总 CPU 耗时适用场景
传统(逐个渲染)NN × ~50μs小规模场景
实例化 (Instancing)1~50μs + 极小额外大量同构对象(树木、建筑)
批处理 (Batching)1~50μs + 合并开销静态几何合并
状态排序 (Sorting)不变不变减少管线状态切换

实例化(Instancing)是 WebGIS 中最常用的 Draw Call 优化手段:将 N 棵相同的树放入一个 draw call,通过 gl_InstanceID 区分每棵树的变换矩阵。

状态排序(State Sorting) 则是另一个容易被忽视的优化点:GPU 管线状态切换(绑定纹理 → 设置 blend mode → 切换 shader)每次都有固定开销。如果渲染顺序是”树 → 石头 → 树 → 石头”,每帧都要切换 4 次状态;如果排序为”树 → 树 → 石头 → 石头”,只需切换 2 次。在 GIS 场景下,合理的渲染顺序通常是:背景瓦片 → 地形瓦片 → 3D 建筑 → 矢量线 → 矢量面 → 标注 → UI。

7. Overdraw 与 Fill Rate

Overdraw(过度绘制)指同一个像素被多次写入。在 3D 地图中,大量半透明图层叠加(大气层 + 云层 + 矢量标注 + UI 面板)会导致同一个像素被着色 5–10 次。GPU 的 Fill Rate(像素填充率)是固定的,Overdraw 越高,有效 Fill Rate 越低。

减少 Overdraw 的方法:

  • 不透明物体从前向后渲染(Early-Z 剔除)
  • 半透明物体从后向前渲染(正确混合),但尽量减少半透明对象数量
  • 使用遮挡查询(Occlusion Query)跳过被遮挡的 3D 建筑

可视化对比与动手实验

实验 1:性能排查表(12 项)

#症状可能根因排查工具修复方案
1瓦片加载卡住网络限流 / CORSDevTools Network调整并发数
2FPS 突然下降Draw call 暴涨Spector.js实例化 / 批处理
3内存持续增长资源未释放Memory Snapshotdispose / 自动清理
4相机位置抖动高精度坐标计算精度丢失检查模型矩阵拆分启用 RTE / 拆分高低精度坐标
5大气层颜色异常光照参数 upload 错误着色器输出颜色检查太阳方向 / 时间参数
6Picking 结果错位拾取纹理与实际帧不同步Picking FBO 截图对比同步渲染时序
7地形坑洞NoData 高程值未处理高程原始数据检查插值 / 父级兜底
8文字模糊字体图集未加载atlas 纹理检查确认字体名 / 重新生成
93D Tiles 位置偏地理锚定矩阵错bounding box 可视化检查变换矩阵乘法顺序
10浏览器崩溃GPU 内存溢出chrome://gpu限制缓存 / 复用纹理
11滚动 / 缩放卡顿主线程长任务阻塞Performance Profile拆分长任务 / 防抖
12触摸不灵敏事件冒泡 / 高频触发Event Listener 面板阻止冒泡 / 节流

实验 2:调试工具对比

工具核心功能适用场景易用性
Chrome DevToolsCPU/内存/网络/GPU 全量分析日常性能监控★★★☆☆
Spector.jsWebGL API 抓帧与回放渲染管线调试★★★☆☆
Stats.js实时 FPS/MS/MB 面板快速性能概览★★★★★
WebGL Inspector纹理/Buffer/Shader 检查资源泄漏排查★★☆☆☆

实验 3:缓存策略对比

策略核心思想命中率复杂度适用场景
LRU淘汰最近最少使用地图漫游、瓦片缓存
LFU淘汰访问频率最低热点数据集
FIFO先进先出极低流式数据、简单队列
ARCLRU + LFU 自适应极高混合访问模式

常见误区

WARNING

误区 1:关闭图层 = 释放资源 关闭图层只是移出场景,材质和纹理不会自动释放。必须显式 dispose。

WARNING

误区 2:Worker 越多越快 实际上 2-4 个最优。太多了线程切换成本超过并行收益。

WARNING

误区 3:浏览器会自动 GC GPU 资源 JS GC 只管理堆内存,不管理 GPU 资源(纹理、Buffer、FBO)。必须显式释放。

WARNING

误区 4:LOD 越细越好 远距离用低 LOD、近距离用高 LOD 才最优。全部最高 LOD 会 GPU 过载。

WARNING

误区 5:把帧时间全部归到 GPU JS 计算时间经常超过 GPU 渲染时间,特别是在复杂地理坐标转换时。

延伸阅读与自测

延伸阅读

  1. WebGL Best Practices - MDNhttps://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices
  2. Spector.jshttps://github.com/BabylonJS/Spector.js
  3. Chrome DevTools Performancehttps://developer.chrome.com/docs/devtools/performance/
  4. High Performance Browser Networkinghttps://hpbn.co/
  5. Stats.jshttps://github.com/mrdoob/stats.js

自测题

  1. 计算题:60 FPS 场景,JS 8ms + 渲染提交 4ms + GPU 7ms = 19ms。是否掉帧?提出 2 个优化方向。
  2. 概念辨析:为什么说”两阶段清理”不能简化为”一次性释放所有资源”?
  3. 方案对比:缓存容量 200,用户持续单向漫游。LRU 和 FIFO 哪个更适合?
  4. 排错实战:地图加载时”瓦片闪烁 + 内存持续增长 + FPS 下降”同时出现,分析根因并列出排查步骤。
  5. 设计题:设计一个自动化性能监控系统,实时采集 FPS、JS/GPU 耗时、内存,FPS < 30 持续 3 秒时告警。写出核心数据结构和伪代码。

下一篇预告11 综合练习 将前面 10 篇的所有知识点串联起来——从底图、地形、矢量标绘到相机飞行、拾取交互,完成一个真实可用的 GIS 应用 Demo,并给出 5+ 扩展练习方向。