[Linux] 新技能:通过代码缓存加速 Node.js 的启动

2022-09-30 | 378 人阅读

前言:之前的文章介绍了通过快照的方式加速 Node.js 的启动,除了快照,V8 还提供了另一种技术加速代码的执行,那就是代码缓存。通过 V8 第一次执行 JS 的时候,V8 需要即时进行解析和编译 JS代码,这个是需要一定时间的,代码缓存可以把这个过程的一些信息保存下来,下次执行的时候,通过这个缓存的信息就可以加速 JS 代码的执行。本文介绍在 Node.js 里如何利用代码缓存技术加速 Node.js 的启动。
首先看一下 Node.js 的编译配置。
  1. 'actions': [
  2.   {
  3.     'action_name': 'node_js2c',
  4.     'process_outputs_as_sources': 1,
  5.     'inputs': [
  6.       'tools/js2c.py',
  7.       '<@(library_files)',
  8.       '<@(deps_files)',
  9.       'config.gypi'
  10.     ],
  11.     'outputs': [
  12.       '<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',
  13.     ],
  14.     'action': [
  15.       '<(python)',
  16.       'tools/js2c.py',
  17.       '--directory',
  18.       'lib',
  19.       '--target',
  20.       '<@(_outputs)',
  21.       'config.gypi',
  22.       '<@(deps_files)',
  23.     ],
  24.   },
  25. ],
复制代码
通过这个配置,在编译 Node.js 的时候,会执行 js2c.py,并且把输入写到 node_javascript.cc 文件。我们看一下生成的内容。
DSC0000.png

里面定义了一个函数,这个函数里面往 source_ 字段里不断追加一系列的内容,其中 key 是 Node.js 中的原生 JS 模块信息,值是模块的内容,我们随便看一个模块 assert/strict。
  1. const data = [39,117,115,101, 32,115,116,114,105, 99,116, 39, 59, 10, 10,109,111,100,117,108,101, 46,101,120,112,111,114,116,115, 32,61, 32,114,101,113,117,105,114,101, 40, 39, 97,115,115,101,114,116, 39, 41, 46,115,116,114,105, 99,116, 59, 10];
  2. console.log(Buffer.from(data).toString('utf-8'))
复制代码
输出如下。
  1. 'use strict';
  2. module.exports = require('assert').strict;
复制代码
通过 js2c.py  ,Node.js 把原生 JS 模块的内容写到了文件中,并且编译进 Node.js 的可执行文件里,这样在 Node.js 启动时就不需要从硬盘里读取对应的文件,否则无论是启动还是运行时动态加载原生 JS 模块,都需要更多的耗时,因为内存的速度远快于硬盘。这是 Node.js 做的第一个优化,接下来看代码缓存,因为代码缓存是在这个基础上实现的。首先看一下编译配置。
  1. ['node_use_node_code_cache=="true"', {
  2.   'dependencies': [
  3.     'mkcodecache',
  4.   ],
  5.   'actions': [
  6.     {
  7.       'action_name': 'run_mkcodecache',
  8.       'process_outputs_as_sources': 1,
  9.       'inputs': [
  10.         '<(mkcodecache_exec)',
  11.       ],
  12.       'outputs': [
  13.         '<(SHARED_INTERMEDIATE_DIR)/node_code_cache.cc',
  14.       ],
  15.       'action': [
  16.         '<@(_inputs)',
  17.         '<@(_outputs)',
  18.       ],
  19.     },
  20.   ],}, {
  21.   'sources': [
  22.     'src/node_code_cache_stub.cc'
  23.   ],
  24. }],
复制代码
如果编译 Node.js 时 node_use_node_code_cache 为 true 则生成代码缓存。如果我们不需要可以关掉,具体执行 ./configure --without-node-code-cache。如果我们关闭代码缓存, Node.js 关于这部分的实现是空,具体在 node_code_cache_stub.cc。
  1. const bool has_code_cache = false;
  2. void NativeModuleEnv::InitializeCodeCache() {}
复制代码
也就是什么都不做。如果我们开启了代码缓存,就会执行 mkcodecache.cc 生成代码缓存。
  1. int main(int argc, char* argv[]) {
  2.   argv = uv_setup_args(argc, argv);
  3.   std::ofstream out;
  4.   out.open(argv[1], std::ios::out | std::ios::binary);
  5.   node::per_process::enabled_debug_list.Parse(nullptr);
  6.   std::unique_ptrplatform = v8::platform::NewDefaultPlatform();
  7.   v8::V8::InitializePlatform(platform.get());
  8.   v8::V8::Initialize();
  9.   Isolate::CreateParams create_params;
  10.   create_params.array_buffer_allocator_shared.reset(
  11.       ArrayBuffer::Allocator::NewDefaultAllocator());
  12.   Isolate* isolate = Isolate::New(create_params);
  13.   {
  14.     Isolate::Scope isolate_scope(isolate);
  15.     v8::HandleScope handle_scope(isolate);
  16.     v8::Localcontext = v8::Context::New(isolate);
  17.     v8::Context::Scope context_scope(context);
  18.     std::string cache = CodeCacheBuilder::Generate(context);
  19.     out << cache;
  20.     out.close();
  21.   }
  22.   isolate->Dispose();
  23.   v8::V8::ShutdownPlatform();
  24.   return 0;
  25. }
复制代码
首先打开文件,然后是 V8 的常用初始化逻辑,最后通过 Generate 生成代码缓存。
  1. std::string CodeCacheBuilder::Generate(Localcontext) {
  2.   NativeModuleLoader* loader = NativeModuleLoader::GetInstance();
  3.   std::vectorids = loader->GetModuleIds();
  4.   std::mapdata;
  5.   for (const auto& id : ids) {
  6.     if (loader->CanBeRequired(id.c_str())) {
  7.       NativeModuleLoader::Result result;
  8.       USE(loader->CompileAsModule(context, id.c_str(), &result));
  9.       ScriptCompiler::CachedData* cached_data = loader->GetCodeCache(id.c_str());
  10.       data.emplace(id, cached_data);
  11.     }
  12.   }
  13.   return GenerateCodeCache(data);
  14. }
复制代码
首先新建一个 NativeModuleLoader。
  1. NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) {
  2.   LoadJavaScriptSource();
  3. }
复制代码
NativeModuleLoader 初始化时会执行 LoadJavaScriptSource,这个函数就是通过 python  生成的 node_javascript.cc 文件里的函数,初始化完成后 NativeModuleLoader 对象的 source_ 字段就保存了原生 JS 模块的代码。接着遍历这些原生 JS 模块,通过 CompileAsModule 进行编译。
  1. MaybeLocalNativeModuleLoader::CompileAsModule(
  2.     Localcontext,
  3.     const char* id,
  4.     NativeModuleLoader::Result* result) {
  5.   Isolate* isolate = context->GetIsolate();
  6.   std::vector<1local> parameters = {
  7.       FIXED_ONE_BYTE_STRING(isolate, "exports"),
  8.       FIXED_ONE_BYTE_STRING(isolate, "require"),
  9.       FIXED_ONE_BYTE_STRING(isolate, "module"),
  10.       FIXED_ONE_BYTE_STRING(isolate, "process"),
  11.       FIXED_ONE_BYTE_STRING(isolate, "internalBinding"),
  12.       FIXED_ONE_BYTE_STRING(isolate, "primordials")};
  13.   return LookupAndCompile(context, id, ¶meters, result);
  14. }
复制代码
接着看 LookupAndCompile
  1. MaybeLocalNativeModuleLoader::LookupAndCompile(
  2.     Localcontext,
  3.     const char* id,
  4.     std::vector<1local>* parameters,
  5.     NativeModuleLoader::Result* result) {
  6.   Isolate* isolate = context->GetIsolate();
  7.   EscapableHandleScope scope(isolate);
  8.   Localsource;
  9.   // 根据 key 从 source_ 字段找到模块内容
  10.   if (!LoadBuiltinModuleSource(isolate, id).ToLocal(&source)) {
  11.     return {};
  12.   }
  13.   std::string filename_s = std::string("node:") + id;
  14.   Localfilename =
  15.       OneByteString(isolate, filename_s.c_str(), filename_s.size());
  16.   ScriptOrigin origin(isolate, filename, 0, 0, true);
  17.   ScriptCompiler::CachedData* cached_data = nullptr;
  18.   {
  19.     Mutex::ScopedLock lock(code_cache_mutex_);
  20.     // 判断是否有代码缓存
  21.     auto cache_it = code_cache_.find(id);
  22.     if (cache_it != code_cache_.end()) {
  23.       cached_data = cache_it->second.release();
  24.       code_cache_.erase(cache_it);
  25.     }
  26.   }
  27.   const bool has_cache = cached_data != nullptr;
  28.   ScriptCompiler::CompileOptions options =
  29.       has_cache ? ScriptCompiler::kConsumeCodeCache
  30.                 : ScriptCompiler::kEagerCompile;
  31.   // 如果有代码缓存则传入            
  32.   ScriptCompiler::Source script_source(source, origin, cached_data);
  33.   // 进行编译
  34.   MaybeLocalmaybe_fun =
  35.       ScriptCompiler::CompileFunctionInContext(context,
  36.                                                &script_source,
  37.                                                parameters->size(),
  38.                                                parameters->data(),
  39.                                                0,
  40.                                                nullptr,
  41.                                                options);
  42.   Localfun;
  43.   if (!maybe_fun.ToLocal(&fun)) {
  44.     return MaybeLocal();
  45.   }
  46.   *result = (has_cache && !script_source.GetCachedData()->rejected)
  47.                 ? Result::kWithCache
  48.                 : Result::kWithoutCache;
  49.   // 生成代码缓存保存下来,最后写入文件,下次使用
  50.   std::unique_ptrnew_cached_data(
  51.       ScriptCompiler::CreateCodeCacheForFunction(fun));
  52.   {
  53.     Mutex::ScopedLock lock(code_cache_mutex_);
  54.     code_cache_.emplace(id, std::move(new_cached_data));
  55.   }
  56.   return scope.Escape(fun);
  57. }
复制代码
第一次执行的时候,也就是编译 Node.js 时,LookupAndCompile 会生成代码缓存写到文件 node_code_cache.cc 中,并编译进可执行文件,内容大致如下。
DSC0001.png

除了这个函数还有一系列的代码缓存数据,这里就不贴出来了。在 Node.js 第一次执行的初始化阶段,就会执行上面的函数,在 code_cache 字段里保存了每个模块和对应的代码缓存。初始化完毕后,后面加载原生 JS 模块时,Node.js 再次执行 LookupAndCompile,就个时候就有代码缓存了。当开启代码缓存时,我的电脑上 Node.js 启动时间大概为 40 毫秒,当去掉代码缓存的逻辑重新编译后,Node.js 的启动时间大概是 60 毫秒,速度有了很大的提升。
总结:Node.js 在编译时首先把原生 JS 模块的代码写入到文件并,接着执行 mkcodecache.cc 把原生 JS 模块进行编译和获取对应的代码缓存,然后写到文件中,同时编译进 Node.js 的可执行文件中,在 Node.js 初始化时会把他们收集起来,这样后续加载原生 JS 模块时就可以使用这些代码缓存加速代码的执行。




[occ]文档来源:网络转载 http://blog.itpub.net/15810651/viewspace-2916851/[/occ]
3426 积分
3425 主题
OPNE在线字典
热门推荐