在日前的 GMTC 全球大前端技术大会上,Rokid 基础平台研发中心的研发工程师刘亚中发表了《JavaScript in IoT》的演讲,本文整理内容如下。
什么是 IoT
如字面翻译,即物联网,但真正要解释起来,其实就是两点:
IoT 是面向服务的 UI。
IoT 面临资源受限的问题。
在物联网时代,我们不再像从前那样独立地使用某个固定、单一的产品,而是在享受着整个环境或者是网络给我们提供的服务,比如原来我们买一个闹钟回来,闹钟就是闹钟,在 IoT 环境下,闹钟是其中的一环,当它叫醒你后,整个系统会为你准备起床后需要的所有待办事物,这就是物联网。
接下来我们来看看资源受限的问题:
最右侧的手机自不必说,整个生态已经相当成熟了。最左侧的是目前的低端配置,可以看到内存和可用空间都是 MB 为单位的,CPU 也相当受限,因此这种设备上 Linux 也已经不满足运行的最低需求了,所以一般是采用更轻量级的 RTOS,流行的有 FreeRTOS 和 RT-Thread(国内)。
最中间的是以 Linux 为主的 128+128 组合的设备,最为我们所熟知的就是智能音箱(无屏),对于这类设备来说包括 Alexa、Rokid、小爱同学等,现在大多数厂商在设备端的开发语言也都以 C + Lua 为主,目前也仅有 Rokid 支持 JavaScript 直接运行在设备端,而本文也主要是针对智能音箱这一挡设备来展开。
然后我们来看下,为什么在 128 MB 的设备上运行 JavaScript 也如此困难呢?
上图是 Rokid 智能音箱上各个子模块的内存占用情况:
kernel 内核占用,用于保证 Linux 用户态程序能正常运行。
dsp 包括从麦克风读出语音数据,然后进行算法处理,最终获取是否语音激活。
system 包括一些底层系统功能,如 IPC 服务、多媒体服务、存储服务等。
applications 即上层应用的逻辑。
通过上图的比例最终可以看出,JavaScript 真正能用的内存只有 25MB,这对于目前一个 Node.js 进程来说也就刚刚好而已,这也是目前大多数音箱,仅在服务器上提供 JavaScript SDK 的原因。
Why JavaScript?
为什么我们要在 IoT 使用 JavaScript 呢?这里简单给出几个理由。首先 Web 已经是一个相当成熟的社区了,它聚集了大量的开发者,这对于任何一个开放平台来说,都是一个非常有吸引力的开发者来源。
另外得益于 JavaScript 或者说 Web 这种即时更新的机制,在解决设备碎片化问题上,有着天然的优势,因此对于 IoT 的碎片化问题,我们希望借助于 Web 来解决。
另外,Web 标准组织已经提出了 WOT —— Web of Things 的概念,因此对于我们来说,要做的就是跟标准组织一起推进 WOT 的落地,这也是一个非常顺理成章的事情。
如果你想参与更多前端技术交流,获取更多专家分享,可以加入我们的“前端技术交流群“,社群内会经常讨论前端相关的技术、分享免费学习资料,我们也会邀请前端专家进行社群分享、直播、公开课等活动。如果你感兴趣,欢迎添加社群管理员微信 GeekUni004,回复“前端群”申请入群。
ShadowNode —— Node.js on IoT
ShadowNode 截止目前为主,已经支持如下特性:
支持 macOS 与 Linux 的编译和运行。
支持 x86、arm 与 aarch64。
支持大部分核心的 Node.js API,如:assert / buffer / N-API Add-ons / child process / crypto / dns / events / file system / http / https / module / net / os / process / timers / TLS / UDP
支持了 WebSocket / MQTT 等流行的 IoT 库
支持 CPU 和 Heap 的 Profiler
支持 N-API
接下来,关于 ShadowNode 的历史大家可以去看之前的专栏文章:
简单来说呢,ShadowNode 就是为了能让 JavaScript 能愉快地跑在 IoT 设备上而存在的,下面是 ShadowNode 与 Node.js 的一些资源占用上的对比:
可以看出无论是 macOS 上,还是 ARM 上,ShadowNode 在内存占用上有明显地提升,还记得我们之前看到智能音箱上各子模块的内存分布吗?对于应用来说可用内存有 25MB,如果使用 ShadowNode 作为运行时的话,基本上就达到了可以随意新增本地应用的状态了。
然后是启动时间,对于设备端上的应用来说,往往为了省内存,会把不重要,或不再使用的应用(进程)杀掉,当有需要时,再重新启动,因此这对进程的启动时间,包括 CPU 占用都提出了不小的要求,可以看出 ShadowNode 在这方面的表现也优于 Node.js。
N-API on ShadowNode
N-API 作为 Node.js Add-on 的 ABI 兼容的接口,可以让任何程序在不需要重新编译的情况下无缝运行在 node-chakracore 与 node-v8 上,ShadowNode 同样如此,我们按照 N-API 的标准文档和测试集,实现了在 ShadowNode 上的 N-API,这样在不需要重新编译的情况下,也能在 Node.js 和 ShadowNode 跨运行时运行,这使得我们可以根据不同的设备配置,选择合适的运行时,而上层的代码则不需要任何修改。
ShadowNode 的 N-API 完全是基于 Node.js 仓库下的 N-API 测试用例测试的,除了一些我们还不支持的特性外,其余的测试用例都会在 ShadowNode CI 上做验证,所以对于稳定性方面大可放心使用。
性能
我们在 IoT 下,针对一些特定场景,也做了一些性能优化。
比如需要使用一些第三方仓库时,仓库本身太过臃肿,导致在设备上加载起来就已经很花时间了(ShadowNode 没有 JIT),甚至于大多数情况是加载不进来的,因为这些库会在堆里创建大量的对象,而 ShadowNode 有预置的 heap_maximum_size,这样难道就没有其他办法了吗?
后面我们想到了一个办法,那就是保持兼容这些第三方库的 JS API,底层全部用 C/C++ 重写,这样可以减少大量的对象创建,同时也能让开发者使用时没有任何差异,我们使用这种方式分别完成了 WebSocket 和 MQTT 在 ShadowNode 上的移植。
另外一个优化手段是引入 NODE_PRIORITIZED_PATH,大家肯定对 NODE_PATH 不陌生吧,那 NODE_PRIORITIZED_PATH 理解起来也不困难,就是一个最高优先级的 NODE_PATH。因为在 IoT 设备上的情况与服务端往往不同,Node.js 模块都是放在每个项目中的,然而在 IoT 设备上的每个应用都比较轻,因此大部分依赖库都是放在全局的 node_modules,这样就导致每次 require 时,总会从模块的当前路径去搜索,这样造成了大量的浪费。
因此我们引入了 NODE_PRIORITIZED_PATH 来设置为全局路径,这样帮我们节省了 30%的启动时间。
接下来是 Copy-on-Write 技术(以下简称 COW),它是 Linux 针对 fork 函数的优化方法,可以节省进程启动时间与内存。这里首先要科普一个知识,即 child_process.fork 并不是真正的 fork,他依然要让子进程从零开始执行,并且抛弃掉父进程的所有资源。
我们来看一下下面的代码:
在 Node.js 中,无论是 spawn、exec 还是 fork,都调用了同一个 uv_spawn 函数,以上是该函数的伪代码(JavaScript 版本)。可以看到每次 fork 完,都会在子进程调用 execvp 来重新初始化子进程,一旦使用了 execvp 这个函数,就意外着系统将把 COW 禁用了,我们再来看看不调用 execvp 的情况下,如何写代码:
上面的代码不再是伪代码了,我们通过将系统的 fork 使用 N-API 暴露给 JavaScript 层,然后从 fork 开始到子进程真正能使用 player、http 和 foobar 模块,仅花了 4ms。
当然,上面的示例代码只是为了证明 COW 拥有卓越的性能提升,因此我们后来就创建了一个新项目 —— Hive。
yodaos-project/hive:https://link.zhihu.com/?target=https%3A//github.com/yodaos-project/hive)
github.com:https://link.zhihu.com/?target=https%3A//github.com/yodaos-project/hive)
Hive 是一个独立于 ShadowNode 的子项目,可以运行在 Node.js 与 ShadowNode 上,因此我们基于 Node.js,对 hive-fork、nodejs-fork、nodejs-spawn 做了一组对比测试:
我们得出的结论显而易见,Hive 在启动速度上几乎是完胜 child_process,这对于设备端的应用启动加速具有很大的意义。
同时在 FaaS 时代,我相信 Hive 也能占有一席之地,FaaS 典型的场景就是快速启动一个进程来执行,然后退出,使用 Hive 可以轻松地定义脚本中的 API,定义好之后,便不需要担心进程启动后所带来的加载消耗,因为几乎是 0 成本的。
YodaOS 从诞生的第一个版本,到现在已经快 1 年时间了。接下来小小预告一下,下半年我们准备发布第二个大版本,即 YodaOS 8.0。在这个版本中,YodaOS 与 Rokid 开放平台完成了解耦,本地的应用也采用了大家熟悉的 Web 应用(不完全兼容)。届时,开发者可以使用 YodaOS 来接入 Alexa、天猫精灵或者 Google Assistant。
总结
很开心能够参加 GMTC 这样的活动,不管是作为讲师还是听众,都收获颇多,我相信 JavaScript 一定会在 IoT 时代释放更多开发者的能量。
嘉宾介绍
刘亚中,开源爱好者,Node.js Collaborator、ShadowNode 作者,目前主攻:Node.js 在 AIoT 领域的应用, 并负责 YodaOS 的社区推广工作。五年 JavaScript 开发经验,曾就职于:SeedMail、Pixbi、阿里巴巴,目前就职于 Rokid 基础平台研发中心,主要工作方向为基于 JavaScript 的物联网操作系统。近年来参与并负责 YodaOS 项目,将 IoT 和 AI 能力开放给 Node.js 社区,并实现了 Node.js 在 IoT 场景的产品级落地。同时也是 Node.js Collaborator、ShadowNode 和 TensorFlow-Node.js 作者,开源发烧友,目前给 60 个开源项目贡献过代码。
评论