GMTC 2019 | 我们为什么要在 IoT 上使用 JavaScript?

前段时间 Rokid 研发工程师 Yorkie 受 GMTC 未来移动技术出品人的邀请,去北京分享了 JavaScript in IoT,希望大家能从他的分享中有所收获。

第一次参加 GMTC 这种比较大的会议,现场有很多值得听的演讲。在进入正题之前,我先分享一下参加这次活动的体会。

我先去听了 Qigsaw 的分享( https://github.com/iqiyi/Qigsaw),它是 Android App Bundles 在国内的解决方案,通过提供了兼容 Android 的 SDK 和服务,并且支持到 Android 4.0,这对于动态化方案来说,已经是十分方便了。

接着是一家上海游戏公司 CTO 的分享,讲了 Web-IR,其实名字看上去就知道是 IR 了,但是当他分享他们已经使用 WebAssembly 把很多原生游戏都移植到 Web 平台上时,我真的很佩服他们,希望他们继续加油!

好,接下来就开始进入本文的正题,JavaScript in IoT!


首先,什么是 IoT

如字面翻译,即物联网,但真正要解释起来,其实就是两点:

  1. IoT 是面向服务的 UI
  2. IoT 面临资源受限的问题

在物联网时代,我们不再像从前那样独立地使用某个固定、单一的产品,而是在享受着整个环境或者是网络给我们提供的服务,比如原来我们买一个闹钟回来,闹钟就是闹钟,在 IoT 环境下,闹钟是其中的一环,当它叫醒你后,整个系统会为你准备起床后需要的所有待办事物,这就是物联网。

接下来我们来看看资源受限的问题:

GMTC 2019 | 我们为什么要在 IoT 上使用 JavaScript?_第1张图片

最右侧的手机自不必说,整个生态已经相当成熟了。最左侧的是目前的低端配置,可以看到内存和可用空间都是 MB 为单位的,CPU 也相当受限,因此这种设备上 Linux 也已经不满足运行的最低需求了,所以一般是采用更轻量级的 RTOS,流行的有 FreeRTOS 和 RT-Thread(国内)。

最中间的是以 Linux 为主的 128+128 组合的设备,最为我们所熟知的就是智能音箱(无屏),对于这类设备来说包括 Alexa、Rokid、小爱同学等,现在大多数厂商在设备端的开发语言也都以 C + Lua 为主,目前也仅有 Rokid 支持 JavaScript 直接运行在设备端,而本文也主要是针对智能音箱这一挡设备来展开。

然后我们来看下,为什么在 128 MB 的设备上运行 JavaScript 也如此困难呢?

GMTC 2019 | 我们为什么要在 IoT 上使用 JavaScript?_第2张图片

上图是 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 的落地,这也是一个非常顺理成章的事情。


ShadowNode —— Node.js on IoT

ShadowNode 截止目前为主,已经支持如下特性:

  1. 支持 macOS 与 Linux 的编译和运行
  2. 支持 x86、arm 与 aarch64
  3. 支持大部分核心的 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
  4. 支持了 WebSocket / MQTT 等流行的 IoT 库
  5. 支持 CPU 和 Heap 的 Profiler
  6. 支持 N-API

接下来,关于 ShadowNode 的历史大家可以去看之前的专栏文章:

Yorkie:ShadowNode v0.8.0 发布​zhuanlan.zhihu.com

Yorkie:ShadowNode v0.9.0 发布​zhuanlan.zhihu.com

Yorkie:ShadowNode v0.10.0 发布(中秋特辑)​zhuanlan.zhihu.com

简单来说呢,ShadowNode 就是为了能让 JavaScript 能愉快地跑在 IoT 设备上而存在的,下面是 ShadowNode 与 Node.js 的一些资源占用上的对比:

GMTC 2019 | 我们为什么要在 IoT 上使用 JavaScript?_第3张图片

可以看出无论是 macOS 上,还是 ARM 上,ShadowNode 在内存占用上有明显地提升,还记得我们之前看到智能音箱上各子模块的内存分布吗?对于应用来说可用内存有25MB,如果使用 ShadowNode 作为运行时的话,基本上就达到了可以随意新增本地应用的状态了。

GMTC 2019 | 我们为什么要在 IoT 上使用 JavaScript?_第4张图片

然后是启动时间,对于设备端上的应用来说,往往为了省内存,会把不重要,或不再使用的应用(进程)杀掉,当有需要时,再重新启动,因此这对进程的启动时间,包括 CPU 占用都提出了不小的要求,可以看出 ShadowNode 在这方面的表现也优于 Node.js。

N-API on ShadowNode

GMTC 2019 | 我们为什么要在 IoT 上使用 JavaScript?_第5张图片

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,他依然要让子进程从零开始执行,并且抛弃掉父进程的所有资源。

我们来看一下下面的代码:

function uv_spawn (file, args) {
  var pid = fork()
  if (pid === 0) {
    execvp(file, args) // this disables COW
    // starting VM and load script
  }
}
uv_spawn(‘test.js’, [])

在 Node.js 中,无论是 spawn、exec 还是 fork,都调用了同一个 uv_spawn 函数,以上是该函数的伪代码(JavaScript 版本)。可以看到每次 fork 完,都会在子进程调用 execvp 来重新初始化子进程,一旦使用了 execvp 这个函数,就意外着系统将把 COW 禁用了,我们再来看看不调用 execvp 的情况下,如何写代码:

var fork = require(‘linux-sys’).fork

// load common modules for children
var player = require(‘player’)
var http = require(‘http’)
var foobar = require(‘foobar’)

// start forking
var pid = fork()
if (pid == 0) {
  // here is the child process
  // use player / http / foobar
}

上面的代码不再是伪代码了,我们通过将系统的 fork 使用 N-API 暴露给 JavaScript 层,然后从 fork 开始到子进程真正能使用 player、http 和 foobar 模块,仅花了4ms。

当然,上面的示例代码只是为了证明 COW 拥有卓越的性能提升,因此我们后来就创建了一个新项目 —— Hive。

yodaos-project/hive​github.com

Hive 是一个独立于 ShadowNode 的子项目,可以运行在 Node.js 与 ShadowNode 上,因此我们基于 Node.js,对 hive-fork、nodejs-fork、nodejs-spawn 做了一组对比测试:

GMTC 2019 | 我们为什么要在 IoT 上使用 JavaScript?_第6张图片

我们得出的结论显而易见,Hive 在启动速度上几乎是完胜 child_process,这对于设备端的应用启动加速具有很大的意义。

同时在 FaaS 时代,我相信 Hive 也能占有一席之地,FaaS 典型的场景就是快速启动一个进程来执行,然后退出,使用 Hive 可以轻松地定义脚本中的 API,定义好之后,便不需要担心进程启动后所带来的加载消耗,因为几乎是0成本的。

Yorkie:YodaOS:一个属于 Node.js 社区的操作系统​zhuanlan.zhihu.com

YodaOS 从诞生的第一个版本,到现在已经快 1 年时间了。接下来小小预告一下,下半年我们准备发布第二个大版本,即 YodaOS 8.0。在这个版本中,YodaOS 与 Rokid 开放平台完成了解耦,本地的应用也采用了大家熟悉的 Web 应用(不完全兼容)。届时,开发者可以使用 YodaOS 来接入 Alexa、天猫精灵或者 Google Assistant。

最后

很开心能够参加 GMTC 这样的活动,不管是作为讲师还是听众,都收获颇多,我相信 JavaScript 一定会在 IoT 时代释放更多开发者的能量。

你可能感兴趣的:(JavaScript,IoT,YodaOS)