You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
_moduleCompile(filename, content) {
// https://github.com/nodejs/node/blob/v7.5.0/lib/module.js#L511
// Remove shebang
var contLen = content.length;
if (contLen >= 2) {
if (content.charCodeAt(0) === 35/*#*/ &&
content.charCodeAt(1) === 33/*!*/) {
if (contLen === 2) {
// Exact match
content = '';
} else {
// Find end of shebang line and slice it off
var i = 2;
for (; i < contLen; ++i) {
var code = content.charCodeAt(i);
if (code === 10/*\n*/ || code === 13/*\r*/) break;
}
if (i === contLen) {
content = '';
} else {
// Note that this actually includes the newline character(s) in the
// new output. This duplicates the behavior of the regular
// expression that was previously used to replace the shebang line
content = content.slice(i);
}
}
}
}
// create wrapper function
var wrapper = Module.wrap(content);
var invalidationKey = crypto
.createHash('sha1')
.update(content, 'utf8')
.digest('hex');
var buffer = this._cacheStore.get(filename, invalidationKey);
var script = new vm.Script(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true,
cachedData: buffer,
produceCachedData: true,
});
if (script.cachedDataProduced) {
this._cacheStore.set(filename, invalidationKey, script.cachedData);
} else if (script.cachedDataRejected) {
this._cacheStore.delete(filename);
}
var compiledWrapper = script.runInThisContext({
filename: filename,
lineOffset: 0,
columnOffset: 0,
displayErrors: true,
});
return compiledWrapper;
}
const compiledWrapper = self._moduleCompile(filename, content);
// We skip the debugger setup because by the time we run, node has already
// done that itself.
const args = [mod.exports, require, mod, filename, dirname, process, global];
return compiledWrapper.apply(mod.exports, args);
背景知识
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
消费生成的 cachev8 是 c++ 代码,在 node 中有对应的 vm.Script 与上面的功能相对应。我们需要对生成的 code cache 设计一套持久化机制,来方便二次消费。
require hook
以上解决了code cache 生成和消费的问题,但是源码从哪里来呢?
我们知道 node.js 通过 require 来连接代码,那么对所有 require 的 module 进行编译并缓存结果是不是就可以了?当然。
v8-compile-cache 便是这样一个仓库- 对编译中间过程持久化,加快整体执行时间。
源码分析
v8-compile-cache
的使用很简单:没有赋值,也没有实例化。我们去源码中看看这段引用究竟执行了什么。
从入口出发
一个 npm 包的入口文件,在
package.json
的main
字段:v8-compile-cache.js。require('v8-compile-cache') 就相当于执行了这段脚本。
抛开最上层的定义,在 module.exports 前发现这样一段代码:
首先检测用户是否通过环境变量
DISABLE_V8_COMPILE_CACHE
禁用了此功能。由于没有实例化的过程。用户可以通过配置此变量来决定是否开启 code caching。接下来我们看一下
supportsCachedData
方法:这里其实是一段对 chakracore 的兼容。这里比较巧妙的是,通过一段空的代码,来验证是否支持 code caching。
解决持久化的 blobStore
在做持久化(其实就是写硬盘)之前,我们需要决定写在哪里。
我们忽略 chakracore 兼容的情况下,getCacheDir 返回一个类似于
/tmp/v8-compile-cache-0/6.2.414.54
地址。其中后一段数字是 v8 的版本,防止不同版本的中间文件不一致导致运行问题。默认中间文件会生成到这个地址。
有了这个写入地址后,我们需要一个类来管理写入:
我们先来看一下 prefix 是什么。
module.parent.filename 返回调用者地址绝对路径或者当前路径。作为 prefix 传递给 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 二进制的映射。接下来我们看
我们先看 install 部分。install 重写了原生模块的 _compile 方法。并提供了一个新的 require 方法。为什么重写 require 呢?因为原生的模块内部也维护了一个 require 方法,既然重写了 _compile ,自然要提供一个 require 给后续使用。
重写 require 部分的代码比较容易理解。_compile 内部又调用了类的 _moduleCompile 方法:
忽略 shebang 部分代码。 先创建一个 wrapper function。
给 vm.Script 去执行。
然后对文件内容生成散列。invalidationKey 如:
cc0579eda025ac6d18f3914d42ba60abe2b1a8e
。接着从内存中取已经生成 code cache。this._cacheStore.get(filename, invalidationKey) 。
vm.Script 第一个参数是 code string。 也就是我们包装过的代码 warapper。第二个参数是 options。其中
cachedData 是编译好的 code cache, 如果没有提供 cacheData 的话,produceCachedData 指示是否输出 code cache。
vm.Script 并不会运行脚本,只负责编译。
如果生成了 code cache ,则写入到内存缓存中, 有问题则删掉缓存。
接下来运行其中的代码。返回一个 compiledWraper。最后包装到 module.exports:
compiledWrapper 返回结果其实就是一个封装好的函数, 如:
在执行 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 内存中:
cacheStore 会将 code cache 写入到 _memoryBlobs 中。并标记 _dirty 为 true, 表示内存中有更新。
到这个时候,所有的变更都在内存中,我们需要写入到硬盘。在什么时机呢?
当进程退出时,如果内存中有更新,就写入到文件中。
在 save 方法中,先调用了 _getDump 方法,内部细节不在赘述。最终写入到机器的 MAP 文件类似如下:
以文件名为 key, 对应 invalidationKey, 在 buffer 文件中的起始位,和结束位。
而 buffer 文件,则存储的是所有 code cache 的二进制文件。
以上。
总结
参考
The text was updated successfully, but these errors were encountered: