前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我把文件重新编码后,加载速度提升300%!

我把文件重新编码后,加载速度提升300%!

作者头像
沐洒
发布2023-07-05 17:10:24
4110
发布2023-07-05 17:10:24
举报
文章被收录于专栏:沐洒

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,zintensity(头部元信息告诉我们的)。

那么我们要做的就是逐行扫描点云数据,分别将4个参数转写为二进制数据,存入 DataView 中,再使用NodeJS文件流API createWriteStream 将数据写入目标文件,核心代码如下:

代码语言:javascript
复制
// ...省略部分代码
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('写入成功!'));

原始代码涉及到一些PCD文件头处理等工作,由于这一部份和业务强相关,和本文所聊的压缩关系不大,所以我就把相关代码删了,只留下二进制转码相关的核心代码。

代码很短,应该不难看懂吧?看不懂的评论区举个手,我来给你手摸手教学下。

懒得看或者看不懂的,我作为课代表帮大家把关键点挑出来了,一起看看吧:

1. DataView

2. CreateWriteStream

3. Buffer.from(dataview.buffer)

我先帮大家捋一捋整体流程,大致如下:

为什么我们没有用理想操作模型呢?两个文件流pipe一下,中间加一个转换器做一个编解码,搞定!多简单呀。

那是因为,当前这个场景不合适。

ThreeJS天然支持PCD文件的渲染,但前提是,必须有标准头,也就是这个东西。

有了头部元信息之后,剩下的部分就是「点云二进制」数据,ThreeJS天然支持。可以看下对应的源码:(https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/PCDLoader.js)

既然如此,那我们就可以把一个带有标准头的二进制文件直接丢给ThreeJS去渲染即可,ThreeJS会在运行时去解析,我们无需在服务端或者前端做多余的「解码」操作,节约渲染成本。

而如果采用理想模型,这意味着我们在转码Stream的每一个chunk的时候,是直接将chunk转成了二进制,并没有按「」为单位的去处理,毕竟NodeJS的chunk是按某个固定字节大小来分片的,而不是定制化的按「点」为分片单位。

这样的话,最终转出来的,仅仅是一个二进制文件,而不是一个ThreeJS可以识别的「点云二进制」文件,我们就必须在渲染之前先处理一遍数据,这就不太合适了。

当然了,如果是那么简单就能做的,也就不会有今天这篇文章了,也是因为这个特殊的处理场景,引入了一些比较有意思的知识点,所以才想分享给大家。

首先想想我们为什么要用DataView?DataView又是什么?一起看下MDN的解释:

DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序(endianness)问题。

我们需要以「」为单位做编码,写入文件,那么也就是说,我们需要操作文件Buffer,而NodeJS为了防止安全和内存泄漏问题,是不允许直接操作Buffer本身的,于是提供了一个DataView接口,非常方便的操作ArrayBuffer

我们可以将每个「点」的信息逐一写入DataView:

代码语言:javascript
复制
point.forEach((axis, axisIndex) => {
  dataview.setFloat32(rowIndex * POINT_BYTES_SIZE + axisIndex * PARAMS_LENGTH, Number(axis), true)
})

这里我们用了setFloat32 ,因为xyzi四个参数每个都是4字节的浮点数,所以用float32来存,第一个参数是位偏移量,第二个是需要存的值,第三个是字节序(可选)。

代码语言:javascript
复制
dataview.setFloat32(byteOffset, value [, littleEndian])

对所有的点逐一处理之后,我们就拿到了一个存有完整点云信息的DataView,就可以拿去写文件了!

But!这里又有关键点了。你以为直接拿DataView的Buffer写入文件即可,如下:

代码语言:javascript
复制
wstream.write(dataview.buffer)

然后你就会看到报错:

为什么呢?

DataView的Buffer难道不是Buffer么?还真不是!你把鼠标挪到代码上一看:

好家伙,是个ArrayBuffer!没错,官方文档一开始就说了,DataView就是拿来操作ArrayBuffer的,没毛病。

那么怎么处理呢?其实稍微改一下就好啦:

代码语言:javascript
复制
wstream.write(Buffer.from(dataview.buffer))

Buffer.from 就可以直接把buffer转出来。

你可能有疑问了,从arrayBuffer转buffer我会了,那反过来呢?哈哈,其实一开始我们读源文件拿来做点云信息解析的时候,就已经这么干了,被我省略了代码,这里补上:

代码语言:javascript
复制
const inputData = fs.readFileSync(input)
const ab = inputData.buffer.slice(inputData.byteOffset, inputData.byteOffset + inputData.byteLength);

看到没有,读取文件数据后,我们拿到的是一个buffer,而从buffer转arrayBuffer,只需要一个小小的slice即可,是不是很神奇。

到这里,其实我帮大家挑出来的关键点就都扯清楚了,最后还剩一个疑问,为啥要用createWriteStream 来写文件?有什么特殊考虑吗?

没错,其实一开始是想着,用「流」来写文件,性能更好,不会占用大量内存空间

但是上面也说了,我们不能简单的对接两头文件流,必须得把「」数据读出来逐个编码,那么势必就需要产生Buffer占用空间了,这样的话,Stream其实就变鸡肋了,完全可以直接替换成 fs.writeFileSync,而且writeFile还可以直接写DataView,更省事了呢!

代码语言:javascript
复制
fs.writeFileSync(output, dataview)

帮大家总结一下今天的几个小知识点:

1. NodeJS提供了一个DataView,用来操作Buffer,确切的说是 ArrayBuffer。 2. Buffer和ArrayBuffer可以互相转换。 3. Stream可以节约Buffer占用内存空间,但如果不利用好pipe,就等于失去了它的优势,还不如直接file操作。

好啦!今天的分享就到这了,你学废了吗?


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-06-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 沐洒 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档