第 26 章:大数据性能——先算规模,再定位瓶颈
学习目标
体数据和表面网格很容易大到浏览器感到吃力。本章建立一套可重复的性能检查方法:估算原始数据大小,区分网络、解析、过滤、GPU 上传、绘制和交互成本,选择合适的预览与质量设置,并说明哪些优化会改变数据含义。读完后,你应该能回答“到底慢在哪里”,而不是看到卡顿就随意降低画质。
26.1 从元组数和字节数开始
规则体数据有 Nx×Ny×Nz 个采样点,若每点有 C 个分量,每个分量占 B 字节,单个 Point Data 数组的原始大小为:
bytes = Nx × Ny × Nz × C × B
例如,512³ 个单分量 Float32 值占 512³×4=536870912 字节,即 512 MiB。两个这样的数组仅原始数值就约 1 GiB,尚未计入 HTTP 下载缓冲、vtk.js 对象、过滤输出、WebGL 纹理、浏览器和系统额外内存。压缩能减少网络传输量,解压后仍要承受原始数值和 GPU 资源成本。
Point Data 数量由点数决定,Cell Data 数量通常由单元数决定。规则 N×N×N 点网格有 (N−1)³ 个体素单元,不能用它为 Point Data 算字节数。向量场三分量 Float32 的字节数是同尺寸单分量场的三倍;若文件同时有多个时间步,累计量还会增加。
第 8 阶段示例提供 32³ 和 64³ 两档单分量 Float32 输入,分别是 128 KiB 与 1 MiB。边长翻倍,原始数据量和遍历全部体素的工作量约为八倍;等值面实际输出的三角形数量则由场的形状和阈值决定,不必正好八倍。
26.2 把一次操作分成六段
| 阶段 | 典型可见现象 | 首选观察方法 |
|---|---|---|
| 网络传输 | 首次打开久,缓存后快 | DevTools Network:URL、状态码、传输大小、等待时间 |
| 解码与数据对象构造 | 文件已到但页面仍停顿 | Performance 主线程记录,解析前后计时 |
| Filter 计算 | 拖阈值、裁剪平面时卡住 | performance.now() 包住 getOutputData();看输出单元数 |
| CPU 到 GPU 上传 | 数据一换,首帧慢 | Performance 帧时间、纹理/缓冲创建;比较首帧与稳定帧 |
| 绘制与体采样 | 相机旋转持续慢 | 帧率、渲染分辨率、体采样设置和目标设备 GPU |
| 内存压力与回收 | 多次换数据后突发停顿或标签页崩溃 | 浏览器 Memory、任务管理器,检查旧数据是否仍被引用 |
同一个“卡一下”可能跨多个阶段。renderWindow.render() 返回的 JavaScript 耗时不等于 GPU 完成整帧的耗时,WebGL 命令可能异步执行。阶段示例测的仅是同步 iso.getOutputData() 调用时间,包含 vtk.js 的管线更新检查;不要把它当作最终 FPS。计时还会受首次 JIT、缓存、控制台、后台标签页和机器负载影响,应在同一环境重复测几次并记录条件。
const t0 = performance.now();
const surface = iso.getOutputData();
const filterMs = performance.now() - t0;
console.log(filterMs, surface.getNumberOfCells());
第一次计算与完全未修改时的再次调用不可直接比较:后者可能复用缓存。若要比较两个分辨率的计算成本,保持算法、阈值、场的物理形状、浏览器窗口和设备相同,并在两次操作中都确实触发重算。阶段示例的 32³ 和 64³ 采样相同物理区域,间距分别为 1 和 0.5,便于观察分辨率提高的代价。
26.3 等值面与过滤链的成本
vtkImageMarchingCubes 要查看三维网格中的许多单元,输入边长变大使遍历工作快速增加。输出 vtkPolyData 还会持有点坐标、三角形连接和可选法线;其内存取决于表面复杂度、噪声与 mergePoints 设置。若随后再接 vtkClipPolyData,改变等值面阈值会让等值面和裁剪都重算;只改变裁剪平面通常只需重算裁剪阶段。
交互控件应匹配计算代价。第 8 阶段示例的阈值滑块在 input 时仅更新显示数字,在 change(松开或确认)时才调用 iso.setContourValue() 并取新网格。这样拖动中不会每个中间值都产生一套三角形。若应用确实需要实时拖动预览,可以考虑限制更新频率、先用低分辨率数据,松开后再做全精度计算;“把计算放进 requestAnimationFrame”本身不会减少每次过滤的工作量。
不要把三角形数当作唯一性能指标:体网格再大、阈值在范围外输出为零,过滤器也可能花时间扫描输入;反过来,带细碎噪声的场可能生成大量小面,拖慢后续 Mapper 和 GPU 上传。记录输入体素数、输出点/单元数与耗时,三者一起看。
26.4 体渲染的两种采样与屏幕成本
第 19–20 章介绍过 vtkVolumeMapper。体渲染的工作量粗略受屏幕上射线数量 × 每条射线采样次数影响:
volumeMapper.setSampleDistance(1.5); // 沿射线走得更远才采一次
volumeMapper.setImageSampleDistance(1.5); // 屏幕平面射线更稀
增大前者可能漏掉薄结构;增大后者可能让图像变粗。交互时可以使用较粗采样,停止拖动后恢复较细采样;也可检查 setAutoAdjustSampleDistances 的行为。maximumSamplesPerRay 限制单条射线的样本数,不能只为提速而把它压低到截断数据。真实空间 spacing、体厚度和相机方向决定需要多密的采样,不能从单个滑块数字推断质量。
降低屏幕渲染分辨率可减少射线或像素工作,但不减少源体数组的下载和存储。同理,改不透明度函数可能让画面更快或更慢,却不是数据降采样。区分“源数据缩小”“过滤输出减少”“每帧绘制变轻”这三类优化,才知道画质和科学含义受什么影响。
26.5 选择性加载与多分辨率
第 23 章的 vtkHttpDataSetReader 能先列出数组,再按名称下载。若只需 Density,就先不要请求 Contrast;这可减少网络与当前数据对象的属性数组占用。但 enableArray 是按整个数组选择,不会自动只取屏幕可见部分或某几个 z 层。已经下载的数组也不会因 enableArray(false) 自动从内存消失。
更大的数据通常需要在数据准备阶段规划:
- 多分辨率:保存预览体和原始体;先显示预览,用户确认区域或质量要求后再加载高分辨率。
- 空间分块或区域请求:只在服务端或文件组织支持时使用,保留块的坐标、spacing、重叠边界与接缝处理。
- 时间步与变量选择:一次只持有当前需要的时刻和属性数组,规划缓存上限。
- 合适的数据类型:如果物理范围和精度允许,可用 16 位整数等类型减小存储;要同时保存比例、偏移、缺失值和单位,不能盲目把 Float32 截成 Uint8。
降采样会改变等值面的位置、细节和小结构。对标量场做平均、取最大值或最近邻,语义不同;标签数据尤其不能随意线性插值出不存在的类别。为性能使用低分辨率时,界面应明确标注预览质量,测量、导出或判断细节时回到合适分辨率的数据。
26.6 控制内存副本和生命周期
在浏览器中,同一份数据可能同时存在为网络响应的 ArrayBuffer、vtkDataArray 的 TypedArray、Filter 输出的点与连接数组,以及 GPU 中的纹理或顶点缓冲。实际副本数量取决于 Reader、过滤器和 WebGL 实现,不能简单说“文件 512 MiB,所以只占 512 MiB”。如果多次切换数据并保留旧 Reader、Mapper、Actor 或事件监听器,内存会继续增长。
释放旧场景时,先弄清哪些对象还被数据管线或 UI 引用。移除不再显示的 Prop、解除无用监听、让旧数据与 Filter 不再被引用,必要时使用 vtk.js 对象的 delete() 生命周期 API;随后通过浏览器 Memory 工具核验是否回收。JavaScript GC 和 GPU 资源释放未必在同一瞬间发生,不应凭一次瞬时内存读数下结论。
26.7 一张实际排错记录表
每次优化前,记录同一操作的:数据维度、数组类型与数量、原始字节数、网络请求数和总量、Filter 输入/输出单元数、同步过滤耗时、首帧和稳定交互是否卡顿、目标浏览器与设备。然后一次只改一类条件,比较画面和数值结果。例如发现 Network 慢,就优先解决数组选择、传输和服务端;发现改阈值慢,就看过滤计算与分辨率;发现相机旋转慢,就看体采样、渲染窗口大小和 GPU。没有测量依据时,“优化”很容易只是在牺牲可解释性。
本章小结
大数据性能先由规模决定上限,再由各阶段的实际耗时决定瓶颈。用原始字节估算和浏览器工具定位成本,按需求选择加载变量、预览分辨率、过滤频率与体采样质量;每一步都要记录它是否改变数据本身。下一章教你在版本变化和陌生对象面前,怎样从示例、API、类型声明和源码独立找到可靠答案。