3个月前,我写过一篇关于性能优化的方法论(《前端性能优化思想模型,在自动驾驶领域的实践》),里面有提到过,我对PCD文件进行二进制转码处理后,效果非常好。
但那篇文章主要是分享方法论和思想模型,并没有展开聊细节,所以估计很多入门小伙伴看都懒得看就划走了,或者看完没有太多感觉,糊里糊涂的也没多大收获。
我写的东西晦涩难懂,帮不到大家,这怎么能行?这是我的耻辱!
那么今天我就拿其中一个案例来展开聊聊,用最通俗的语言,给你抽丝剥茧讲明白。
先看效果!
转码前后文件尺寸对比:(17.8MB vs 4.6MB,压缩率75%)
转码前页面加载效果:(ASCII编码,2倍速播放,18秒)
转码后页面加载效果:(二进制编码,2倍速播放,5秒)
之前也提到过,在自动驾驶点云标注场景下,一次需要加载几十帧的数据文件,如果每一帧文件都是动辄十几二十MB,那即便做异步加载,等待时间之久也是相当令人头大的。
性能优化迫在眉睫!
好,我们先来盘点一下前端手里能用的几个性能优化法宝:
1. 异步加载2. 分片加载,增量渲染3. 资源文件压缩
4. 缓存
本文暂且只讲3,124就先跳过不聊了,之所以摆在这里是想给大家一点启发,告诉你,还有这么些个优化方法呢,感兴趣的评论区交流,最好是关注我,追更,也给我一些动力。
来,我们继续。
聊到文件压缩,不得不提一件有意思的往事。很多人问我,我网名为啥叫ASCII26?
那是因为,大学时候学到著名的哈夫曼编码(Huffman Coding),老师给我们布置了一道作业,用哈夫曼编码压缩一段超长文本,比如一部小说。
我觉得这事儿很有意思,就吭哧吭哧开始写算法,写完一运行,文件确实压小了不少,正得意呢,突然发现,压缩文件反向解码的时候出错了,解出来的文件出现了乱码。
这个问题让我定位了好久好久,没日没夜反复调试,怎么都找不到bug在哪。直到有一天,我发现我编码的文本里有一个鬼东西,原文中肉眼不可见,编码后是一个极其容易被忽视的小红点,我用代码读它,发现这东西的ASCII编码值是26。
当然了,是什么原因导致的bug,以及我怎么解决的,我都记不清了(已经十多年了),我只记得,从那天起,我把所有的网名全都变成了ASCII26,算是一个纪念吧,纪念我呕心沥血独自解决了一个难题,也纪念我对编程这件事仍充满着热爱。
扯这个小故事给大家放松下,同时,这个小故事本身也是编解码相关。
好啦,回到正题。
那么这次我们要编解码什么呢?我们先来看下我们要处理的文件长什么样。
这就是PCD文件(自动驾驶点云文件)的冰山一角,其中,1-11行是它的标准头部信息,而12行之后,便是无穷无尽可随意扩展的点云数据。
这里简单提一嘴,有的产商提供的点云数据直接就是bin文件,而有的是pcd文件,还有的甚至是JSON文件,总之,国内的自动驾驶行业现状非常混乱,工程团队素质良莠不齐。
知道PCD文件头部元信息之后,我们把它取出来备用,这一小部分并不会占用太多体积,压不压缩都无所谓,压缩反而不利于后期直接在ThreeJs里引用。
而真正影响文件体积的,是12行之后的点云数据(几十万行),我们观察这一部份的构成,每一行是一个点的信息,用空格分割,分别代表着x,y,z和intensity(头部元信息告诉我们的)。
那么我们要做的就是逐行扫描点云数据,分别将4个参数转写为二进制数据,存入 DataView 中,再使用NodeJS文件流APIcreateWriteStream 将数据写入目标文件,核心代码如下:
// ...省略部分代码 const PARAMS_LENGTH = 4; // [x, y, z, i].length const AXIS_BYTES_SIZE = 4; const POINT_BYTES_SIZE = PARAMS_LENGTH * AXIS_BYTES_SIZE; // 创建一个dataView,窗口长度初始化为 点云数量 * 16 (从头部文件信息可知,一行4个参数,每个参数占4个字节) const dataview = new DataView(new ArrayBuffer(POINT_BYTES_SIZE * points.length)); points.forEach((pointString, rowIndex) => { // 将点从字符串中取出来,即'x y z i' => [x, y, z, i] const point = pointString.split(' '); // 逐个参数处理,塞入dataview中 point.forEach((axis, axisIndex) => { dataview.setFloat32(rowIndex * POINT_BYTES_SIZE + axisIndex * PARAMS_LENGTH, Number(axis), true) }) }) const wstream = fs.createWriteStream(output, 'binary'); // 写回头部信息 wstream.write(getPCDHeaderString(points.length)) // 写入点云信息 wstream.write(Buffer.from(dataview.buffer)) wstream.end(() => console.log('写入成功!'));