新技能:透過程式碼快取加速 Node.js 的啟動

wang1103392發表於2022-09-29

前言:之前的文章介紹了透過快照的方式加速 Node.js 的啟動,除了快照,V8 還提供了另一種技術加速程式碼的執行,那就是程式碼快取。透過 V8 第一次執行 JS 的時候,V8 需要即時進行解析和編譯 JS程式碼,這個是需要一定時間的,程式碼快取可以把這個過程的一些資訊儲存下來,下次執行的時候,透過這個快取的資訊就可以加速 JS 程式碼的執行。本文介紹在 Node.js 裡如何利用程式碼快取技術加速 Node.js 的啟動。

首先看一下 Node.js 的編譯配置。

'actions': [
  {
    'action_name': 'node_js2c',
    'process_outputs_as_sources': 1,
    'inputs': [
      'tools/js2c.py',
      '<@(library_files)',
      '<@(deps_files)',
      'config.gypi'
    ],
    'outputs': [
      '<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',
    ],
    'action': [
      '<(python)',
      'tools/js2c.py',
      '--directory',
      'lib',
      '--target',
      '<@(_outputs)',
      'config.gypi',
      '<@(deps_files)',
    ],
  },
],

透過這個配置,在編譯 Node.js 的時候,會執行 js2c.py,並且把輸入寫到 node_javascript.cc 檔案。我們看一下生成的內容。
新技能:透過程式碼快取加速 Node.js 的啟動新技能:透過程式碼快取加速 Node.js 的啟動
裡面定義了一個函式,這個函式里面往 source_ 欄位裡不斷追加一系列的內容,其中 key 是 Node.js 中的原生 JS 模組資訊,值是模組的內容,我們隨便看一個模組 assert/strict。

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];
console.log(Buffer.from(data).toString('utf-8'))

輸出如下。

'use strict';
module.exports = require('assert').strict;

透過 js2c.py  ,Node.js 把原生 JS 模組的內容寫到了檔案中,並且編譯進 Node.js 的可執行檔案裡,這樣在 Node.js 啟動時就不需要從硬碟裡讀取對應的檔案,否則無論是啟動還是執行時動態載入原生 JS 模組,都需要更多的耗時,因為記憶體的速度遠快於硬碟。這是 Node.js 做的第一個最佳化,接下來看程式碼快取,因為程式碼快取是在這個基礎上實現的。首先看一下編譯配置。

['node_use_node_code_cache=="true"', {
  'dependencies': [
    'mkcodecache',
  ],
  'actions': [
    {
      'action_name': 'run_mkcodecache',
      'process_outputs_as_sources': 1,
      'inputs': [
        '<(mkcodecache_exec)',
      ],
      'outputs': [
        '<(SHARED_INTERMEDIATE_DIR)/node_code_cache.cc',
      ],
      'action': [
        '<@(_inputs)',
        '<@(_outputs)',
      ],
    },
  ],}, {
  'sources': [
    'src/node_code_cache_stub.cc'
  ],
}],

如果編譯 Node.js 時 node_use_node_code_cache 為 true 則生成程式碼快取。如果我們不需要可以關掉,具體執行 ./configure --without-node-code-cache。如果我們關閉程式碼快取, Node.js 關於這部分的實現是空,具體在 node_code_cache_stub.cc。

const bool has_code_cache = false;
void NativeModuleEnv::InitializeCodeCache() {}

也就是什麼都不做。如果我們開啟了程式碼快取,就會執行 mkcodecache.cc 生成程式碼快取。

int main(int argc, char* argv[]) {
  argv = uv_setup_args(argc, argv);
  std::ofstream out;
  out.open(argv[1], std::ios::out | std::ios::binary);
  node::per_process::enabled_debug_list.Parse(nullptr);
  std::unique_ptrplatform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();
  Isolate::CreateParams create_params;
  create_params.array_buffer_allocator_shared.reset(
      ArrayBuffer::Allocator::NewDefaultAllocator());
  Isolate* isolate = Isolate::New(create_params);
  {
    Isolate::Scope isolate_scope(isolate);
    v8::HandleScope handle_scope(isolate);
    v8::Localcontext = v8::Context::New(isolate);
    v8::Context::Scope context_scope(context);
    std::string cache = CodeCacheBuilder::Generate(context);
    out << cache;
    out.close();
  }
  isolate->Dispose();
  v8::V8::ShutdownPlatform();
  return 0;
}

首先開啟檔案,然後是 V8 的常用初始化邏輯,最後透過 Generate 生成程式碼快取。

std::string CodeCacheBuilder::Generate(Localcontext) {
  NativeModuleLoader* loader = NativeModuleLoader::GetInstance();
  std::vectorids = loader->GetModuleIds();
  std::mapdata;
  for (const auto& id : ids) {
    if (loader->CanBeRequired(id.c_str())) {
      NativeModuleLoader::Result result;
      USE(loader->CompileAsModule(context, id.c_str(), &result));
      ScriptCompiler::CachedData* cached_data = loader->GetCodeCache(id.c_str());
      data.emplace(id, cached_data);
    }
  }
  return GenerateCodeCache(data);
}

首先新建一個 NativeModuleLoader。

NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) {
  LoadJavaScriptSource();
}

NativeModuleLoader 初始化時會執行 LoadJavaScriptSource,這個函式就是透過 python  生成的 node_javascript.cc 檔案裡的函式,初始化完成後 NativeModuleLoader 物件的 source_ 欄位就儲存了原生 JS 模組的程式碼。接著遍歷這些原生 JS 模組,透過 CompileAsModule 進行編譯。

MaybeLocalNativeModuleLoader::CompileAsModule(
    Localcontext,
    const char* id,
    NativeModuleLoader::Result* result) {
  Isolate* isolate = context->GetIsolate();
  std::vector<1local> parameters = {
      FIXED_ONE_BYTE_STRING(isolate, "exports"),
      FIXED_ONE_BYTE_STRING(isolate, "require"),
      FIXED_ONE_BYTE_STRING(isolate, "module"),
      FIXED_ONE_BYTE_STRING(isolate, "process"),
      FIXED_ONE_BYTE_STRING(isolate, "internalBinding"),
      FIXED_ONE_BYTE_STRING(isolate, "primordials")};
  return LookupAndCompile(context, id, ¶meters, result);
}

接著看 LookupAndCompile

MaybeLocalNativeModuleLoader::LookupAndCompile(
    Localcontext,
    const char* id,
    std::vector<1local>* parameters,
    NativeModuleLoader::Result* result) {
  Isolate* isolate = context->GetIsolate();
  EscapableHandleScope scope(isolate);
  Localsource;
  // 根據 key 從 source_ 欄位找到模組內容
  if (!LoadBuiltinModuleSource(isolate, id).ToLocal(&source)) {
    return {};
  }
  std::string filename_s = std::string("node:") + id;
  Localfilename =
      OneByteString(isolate, filename_s.c_str(), filename_s.size());
  ScriptOrigin origin(isolate, filename, 0, 0, true);
  ScriptCompiler::CachedData* cached_data = nullptr;
  {
    Mutex::ScopedLock lock(code_cache_mutex_);
    // 判斷是否有程式碼快取
    auto cache_it = code_cache_.find(id);
    if (cache_it != code_cache_.end()) {
      cached_data = cache_it->second.release();
      code_cache_.erase(cache_it);
    }
  }
  const bool has_cache = cached_data != nullptr;
  ScriptCompiler::CompileOptions options =
      has_cache ? ScriptCompiler::kConsumeCodeCache
                : ScriptCompiler::kEagerCompile;
  // 如果有程式碼快取則傳入             
  ScriptCompiler::Source script_source(source, origin, cached_data);
  // 進行編譯
  MaybeLocalmaybe_fun =
      ScriptCompiler::CompileFunctionInContext(context,
                                               &script_source,
                                               parameters->size(),
                                               parameters->data(),
                                               0,
                                               nullptr,
                                               options);
  Localfun;
  if (!maybe_fun.ToLocal(&fun)) {
    return MaybeLocal();
  }
  *result = (has_cache && !script_source.GetCachedData()->rejected)
                ? Result::kWithCache
                : Result::kWithoutCache;
  // 生成程式碼快取儲存下來,最後寫入檔案,下次使用
  std::unique_ptrnew_cached_data(
      ScriptCompiler::CreateCodeCacheForFunction(fun));
  {
    Mutex::ScopedLock lock(code_cache_mutex_);
    code_cache_.emplace(id, std::move(new_cached_data));
  }
  return scope.Escape(fun);
}

第一次執行的時候,也就是編譯 Node.js 時,LookupAndCompile 會生成程式碼快取寫到檔案 node_code_cache.cc 中,並編譯進可執行檔案,內容大致如下。
新技能:透過程式碼快取加速 Node.js 的啟動新技能:透過程式碼快取加速 Node.js 的啟動
除了這個函式還有一系列的程式碼快取資料,這裡就不貼出來了。在 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 模組時就可以使用這些程式碼快取加速程式碼的執行。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/15810651/viewspace-2916851/,如需轉載,請註明出處,否則將追究法律責任。

相關文章