背景知识
v8 是一个 JIT(Just in time) 编译器。与传统的解释器一行一行执行不同的是,JIT 会在执行脚本前,对源码先解析(parsing)、再编译(compiling),速度相比前者提升了不少。但解析和编译仍然消耗时间。能否将中间结果缓存起来呢?
所以 v8 在 4.2(node > 5.7.0) 时,就支持了 code caching 的功能。减少二次执行的构建时间,加快脚本的整体执行速度。
缓存中间结果,持久化到硬盘
要使用中间文件,需要先生成中间文件。v8 提供了以下几个方法:
v8::ScriptCompiler::kProduceCodeCache
是否生成 code cachev8::ScriptCompiler::Source::GetCachedData
获取生成的 code cachev8::ScriptCompiler::kConsumeCodeCache
消费生成的 cache
v8 是 c++ 代码,在 node 中有对应的 vm.Script 与上面的功能相对应。我们需要对生成的 code cache 设计一套持久化机制,来方便二次消费。
require hook
以上解决了code cache 生成和消费的问题,但是源码从哪里来呢?
我们知道 node.js 通过 require 来连接代码,那么对所有 require 的 module 进行编译并缓存结果是不是就可以了?当然。
v8-compile-cache 便是这样一个仓库- 对编译中间过程持久化,加快整体执行时间。
源码分析
v8-compile-cache
的使用很简单:
1 | require('v8-compile-cache') |
没有赋值,也没有实例化。我们去源码中看看这段引用究竟执行了什么。
从入口出发
一个 npm 包的入口文件,在 package.json
的 main
字段:v8-compile-cache.js。
require(‘v8-compile-cache’) 就相当于执行了这段脚本。
抛开最上层的定义,在 module.exports 前发现这样一段代码:
1 | if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) { |
首先检测用户是否通过环境变量 DISABLE_V8_COMPILE_CACHE
禁用了此功能。由于没有实例化的过程。用户可以通过配置此变量来决定是否开启 code caching。
接下来我们看一下 supportsCachedData
方法:
1 | function supportsCachedData() { |
这里其实是一段对 chakracore 的兼容。这里比较巧妙的是,通过一段空的代码,来验证是否支持 code caching。
解决持久化的 blobStore
在做持久化(其实就是写硬盘)之前,我们需要决定写在哪里。
1 | const cacheDir = getCacheDir(); |
1 | function getCacheDir() { |
我们忽略 chakracore 兼容的情况下,getCacheDir 返回一个类似于 /tmp/v8-compile-cache-0/6.2.414.54
地址。其中后一段数字是 v8 的版本,防止不同版本的中间文件不一致导致运行问题。
1 | mac os 多用户的 tmp 地址为 :`/var/folders/x8/pxgnmcp53gjf3llf69jqb4gh0000gq/T/` |
默认中间文件会生成到这个地址。
有了这个写入地址后,我们需要一个类来管理写入:
1 | const prefix = getParentName(); |
我们先来看一下 prefix 是什么。
1 | function getParentName() { |
module.parent.filename 返回调用者地址绝对路径或者当前路径。作为 prefix 传递给 FileSystemBlobStore。
1 | class FileSystemBlobStore { |
在 new FileSystemBlobStore(cacheDir, prefix)
时,会先把 prefix 通过 slashEscape 转换成一个标准的名字,作为文件名。this._blobFilename 存储生成的二进制 code caching。this._mapFilename 存储脚本到二进制文件的映射。然后 _load 到这些中间文件到内存中。
如果本地没有中间文件, this._storedBlob 则是一个 0 长度的 buffer, this._storedMap 则是一个空对象。
关联 require hook 和 blobStore 的 NativeCompileCache
再上一个过程中,我们建立了一个 blobStore。它会把已经编译(如果有)的中间文件缓存到内存中。并通过 map 来定位文件和 blob 二进制的映射。接下来我们看
1 | const nativeCompileCache = new NativeCompileCache(); |
1 | class NativeCompileCache { |
我们先看 install 部分。install 重写了原生模块的 _compile 方法。并提供了一个新的 require 方法。为什么重写 require 呢?因为原生的模块内部也维护了一个 require 方法,既然重写了 _compile ,自然要提供一个 require 给后续使用。
1 | const compiledWrapper = self._moduleCompile(filename, content); |
重写 require 部分的代码比较容易理解。_compile 内部又调用了类的 _moduleCompile 方法:
1 | _moduleCompile(filename, content) { |
忽略 shebang 部分代码。 先创建一个 wrapper function。
1 | (function(exports, require, module, __filename, __dirname) { |
给 vm.Script 去执行。
然后对文件内容生成散列。invalidationKey 如: cc0579eda025ac6d18f3914d42ba60abe2b1a8e
。
接着从内存中取已经生成 code cache。this._cacheStore.get(filename, invalidationKey) 。
1 | var script = new vm.Script(wrapper, { |
vm.Script 第一个参数是 code string。 也就是我们包装过的代码 warapper。第二个参数是 options。其中
cachedData 是编译好的 code cache, 如果没有提供 cacheData 的话,produceCachedData 指示是否输出 code cache。
vm.Script 并不会运行脚本,只负责编译。
1 | if (script.cachedDataProduced) { |
如果生成了 code cache ,则写入到内存缓存中, 有问题则删掉缓存。
1 | var compiledWrapper = script.runInThisContext({ |
接下来运行其中的代码。返回一个 compiledWraper。最后包装到 module.exports:
1 | const compiledWrapper = self._moduleCompile(filename, content); |
compiledWrapper 返回结果其实就是一个封装好的函数, 如:
1 | function(exports, require, module, __filename, __dirname) { |
在执行 compiledWrapper.apply(mod.exports, args)时, 对 mod 重新赋值,应用了新的 require 生成了新的 module.exports。
最后 return 的结果就是 module.exports 的内容。
而 Module.prototype._load 会将 Module.prototype._compile 返回的结果给用户, 两者的调用机制,可以参考这篇文章。
这样就完成了hook require,并取缓存中 code cache 的流程。
持久化的一些细节
上面只是简略的过了一下持久化的过程。下面进行详细分析。
我们看一下持久化是如何存储的, 在 code cache 生成的过程中,如果满足条件, 先写入到 _cacheStroe 内存中:
1 | if (script.cachedDataProduced) { |
1 | set(key, invalidationKey, buffer) { |
cacheStore 会将 code cache 写入到 _memoryBlobs 中。并标记 _dirty 为 true, 表示内存中有更新。
到这个时候,所有的变更都在内存中,我们需要写入到硬盘。在什么时机呢?
1 | process.once('exit', code => { |
当进程退出时,如果内存中有更新,就写入到文件中。
1 |
|
在 save 方法中,先调用了 _getDump 方法,内部细节不在赘述。最终写入到机器的 MAP 文件类似如下:
1 | {"/Users/flyyang/devspace/test-v8-file-size/b.js":["ba9069dd2de36ca9d7a51fb6f6d2d00c8d4b11a8",0,1064],"/Users/fl |
以文件名为 key, 对应 invalidationKey, 在 buffer 文件中的起始位,和结束位。
而 buffer 文件,则存储的是所有 code cache 的二进制文件。
以上。
总结
- 工具类应用使用此包会加速构建速度。
- 开发,甚至是了解需要对 node 的运行,v8 周边有深入了解。
issue
有问题,来 github 一起讨论。