偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

V8 加速啟動的快照技術(shù)

開發(fā) 前端
如果啟動時傳了 --build_snapshot 表示需要在進程退出時構(gòu)建快照,并且把構(gòu)建的快照寫入到當(dāng)前目錄到 snapshot.blob 文件中。否則就是正常啟動,如果正常啟動時傳了 --snapshop_blob snapshot.blob 則表示通過快照啟動。

V8 每次啟動時都需要創(chuàng)建新的 Isolate 和 Context,這需要一定的時間,V8 啟動快照是一種用于加速啟動性能的技術(shù),它利用事先創(chuàng)建的快照直接初始化 Isolate 和 Context,加快啟動時間。同時 V8 還提供了一系列 API 給使用者,使用者可以把自己的數(shù)據(jù)寫入快照,然后在啟動的時候直接恢復(fù)數(shù)據(jù),加快啟動速度,比如 Node.js 支持啟動快照。我最近給自己之前寫的玩具 JS 運行時 No.js 加入了啟動快照的功能,以此為例簡單介紹下 V8 的快照技術(shù)。

以下 No.js 的啟動代碼。

int main(int argc, char* argv[]) {
  if (argc > 1) {
    if (strcmp(argv[1], "--build_snapshot") == 0) {
      BuildSnapshot(argc, argv);
    } else {
      Start(argc, argv);
    }
  }
  return 0;
}

如果啟動時傳了 --build_snapshot 表示需要在進程退出時構(gòu)建快照,并且把構(gòu)建的快照寫入到當(dāng)前目錄到 snapshot.blob 文件中。否則就是正常啟動,如果正常啟動時傳了 --snapshop_blob snapshot.blob 則表示通過快照啟動。

構(gòu)建快照

void BuildSnapshot(int argc, char* argv[]) {
  {
    // 表示快照的信息
    No::Snapshot::SnapshotData snapshot_data;
    // No.js 實現(xiàn)的 C++ API 的地址數(shù)組
    No::ExternalReferenceRegistry external_reference_registry;
    conststd::vector<intptr_t>& external_references = external_reference_registry.external_references();
    v8::SnapshotCreator creator(external_references.data());
    // 啟動代碼
    Isolate* isolate = creator.GetIsolate();
    {
      Isolate::Scope isolate_scope(isolate);
      HandleScope handle_scope(isolate);
      Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
      Local<Context> context = Context::New(isolate, nullptr, global);
      Context::Scope context_scope(context);
      Environment * env = new Environment(context);
      {
        No::MicroTask::MicroTaskScope microTaskScope(env);
        // 執(zhí)行用戶 JS 代碼,啟動事件循環(huán)
        No::Core::Run(env);
      }
      // 執(zhí)行完畢,處理快照的邏輯
      env->run_snapshot_serial_callback();
      // 設(shè)置上下文到快照中
      creator.SetDefaultContext(context, v8::SerializeInternalFieldsCallback());
      // 把 No.js 內(nèi)部的信息寫入快照
      env->serialize(&creator, &snapshot_data);
      delete env;
    }
    // 創(chuàng)建快照
    snapshot_data.blob = creator.CreateBlob(v8::SnapshotCreator::FunctionCodeHandling::kKeep);
    // 把快照內(nèi)存寫到文件
    // ...
}

構(gòu)建快照的過程和一般的啟動過程是類似的,區(qū)別是在進程退出前會執(zhí)行構(gòu)建和寫快照的步驟。下面來看下構(gòu)建快照的過程。creator.SetDefaultContext 會把當(dāng)前執(zhí)行的 context 加入到快照中,假設(shè)我們在 JS 里設(shè)置了一個全局變量,那么后續(xù)通過快照啟動時,該全局變量還會存在,可以直接使用,除了把 V8 context 加入快照,No.js 還會把自己相關(guān)的信息加入快照中。

void Environment::serialize(v8::SnapshotCreator* creator, No::Snapshot::SnapshotData* snapshot_data) {  
    uint32_t id = 0;
    #define V(PropertyName, TypeName)                                              \
        if (!PropertyName().IsEmpty()) {                                 \
            size_t index = creator->AddData(GetContext(), PropertyName());         \
            snapshot_data->env_info.props.push_back({#PropertyName, index, id++}); \
        }       
        PER_ISOLATE_TEMPLATE_PROPERTIES(V)
        PER_ISOLATE_OBJECT_PROPERTIES(V)
        PER_ISOLATE_FUNCTION_PROPERTIES(V)
    #undef V
}

Environment 是 No.js 運行時用到的一些公共信息,serialize 里是把 Environment 的各種信息加入到快照中,下面看一個例子。

if (!snapshot_serialize_cb.IsEmpty()) {                                 
    size_t index = creator->AddData(GetContext(), snapshot_serialize_cb());         
    snapshot_data->env_info.props.push_back({"snapshot_serialize_cb", index, id++}); 
}

上面代碼通過 V8 的 AddData API 把 V8 對象加入到快照中,V8 會返回一個索引,我們需要記錄這個索引,后續(xù)通過快照啟動時需要用到(這是實現(xiàn)快照加速非常關(guān)鍵的邏輯)。執(zhí)行完這些操作后,最后通過 V8 的 creator.CreateBlob API 創(chuàng)建快照,然后寫入文件中。

std::ofstream out("snapshot.blob", std::ios::out | std::ios::binary);
// 寫入一些元信息
for (size_t i = 0; i < snapshot_data.env_info.props.size(); i++) {
    std::string name = snapshot_data.env_info.props[i].name;
    std::string id = std::to_string(snapshot_data.env_info.props[i].id);
    std::string index = std::to_string(snapshot_data.env_info.props[i].index);
    out.write(name.data(), name.size());
    out.write(":", 1);
    out.write(id.data(), id.size());
    out.write(":", 1);
    out.write(index.data(), index.size());
    if (i != (snapshot_data.env_info.props.size() - 1)) {
    out.write("|", 1);
    }
}
out.write("\n", 1);
// 寫入 V8 快照信息
out.write(snapshot_data.blob.data, snapshot_data.blob.raw_size);
out.close();

前面說過我們把額外的信息加入 V8 快照時會返回一個索引,我們需要收集這些索引并寫入文件中,后續(xù)通過快照啟動時使用,否則無法從快照中找到我們需要的信息。

通過快照啟動

有了快照后,我們就可以通過快照啟動了。

void Start(int argc, char* argv[]) {
  Isolate::CreateParams create_params;
  No::Snapshot::SnapshotData snapshot_data;
  No::ExternalReferenceRegistry external_reference_registry;
bool startup_from_snapshot = false;
// 通過快照啟動
if (strcmp(argv[1], "--snapshot_blob") == 0) {
    //  buffer 表示快照的內(nèi)容
    std::string s(buffer.data(), buffer.size());
    int end = s.find("\n"); 
    // 讀取 No.js 自己寫入的索引元信息
    std::string prop_data = s.substr(0, end);
    std::vector<std::string> props = No::Util::Split(prop_data, '|');
    for (auto& prop : props) {
      std::vector<std::string> prop_fields = No::Util::Split(prop, ':');
      snapshot_data.env_info.props.push_back({prop_fields[0],static_cast<size_t>(std::stoi(prop_fields[1])), static_cast<uint32_t>(std::stoi(prop_fields[2]))});
    }
    // 讀取 V8 的快照信息
    s = s.substr(end + 1);
    char * blob = newchar[s.size()];
    memcpy(blob, s.data(), s.size());
    snapshot_data.blob = v8::StartupData{blob, static_cast<int>(s.size())};
    // 設(shè)置到啟動參數(shù) create_params 重
    create_params.snapshot_blob = &snapshot_data.blob;
    conststd::vector<intptr_t>& external_references = external_reference_registry.external_references();
    create_params.external_references = external_references.data();
    startup_from_snapshot = true;
  }

  Isolate* isolate = Isolate::New(create_params);
  {
    Isolate::Scope isolate_scope(isolate);
    HandleScope handle_scope(isolate);
    // 創(chuàng)建 Context,因為 create_params 中設(shè)置了快照,這里會直接從快照構(gòu)建 Context,而不是重新創(chuàng)建
    Local<Context> context = Context::New(isolate);
    Context::Scope context_scope(context);
    Environment * env = new Environment(context);
    // 從快照中獲取之前設(shè)置到信息
    env->deserialize(&snapshot_data);
    env->run_snapshot_deserial_callback();
    // 正常啟動,執(zhí)行 JS 和事件循環(huán)
    {
      No::MicroTask::MicroTaskScope microTaskScope(env);
      if (startup_from_snapshot && !env->snapshot_deserialize_main().IsEmpty()) {
        env->run_snapshot_deserialize_main();
      } else {
        No::Core::Run(env);
      }
    }
    delete env;
  }
}

首先讀取快照文件內(nèi)容,內(nèi)容分為兩部分,分別是 No.js 本身的元信息和 V8 的快照內(nèi)容,前者 No.js 自己使用,后者提供給 V8 用于啟動初始化,等 V8 初始化完畢后我們通過 env->deserialize 從快照中恢復(fù)額外的數(shù)據(jù)。

void Environment::deserialize(No::Snapshot::SnapshotData* snapshot_data) {  
    #define V(PropertyName, TypeName)                                              \
        for (auto& prop : snapshot_data->env_info.props) {\
            if (prop.name == #PropertyName) {\
                Local<TypeName> obj = GetContext()->GetDataFromSnapshotOnce<TypeName>(prop.index).ToLocalChecked();\
                set_##PropertyName(obj);\
            }\
        }
        PER_ISOLATE_TEMPLATE_PROPERTIES(V)
        PER_ISOLATE_OBJECT_PROPERTIES(V)
        PER_ISOLATE_FUNCTION_PROPERTIES(V)
    #undef V
}

以上代碼大致就是執(zhí)行 context->GetDataFromSnapshotOnce(prop.index) API 并傳入對應(yīng)的索引從快照中獲取之前設(shè)置的信息并設(shè)置到 Environment 中,這樣就完成了數(shù)據(jù)的恢復(fù)。

內(nèi)部模塊快照

JS 運行時有很多內(nèi)置模塊,每次執(zhí)行時都需要編譯執(zhí)行浪費時間,我們可以把它直接寫入快照中,后續(xù)通過快照啟動時直接使用,避免重復(fù)處理。下面看了一個例子。

const snapshot = require("snapshot");

if (snapshot.isBuildSnapshot()) {
    snapshot.hello="world"
} else {
    console.log(snapshot.hello)
}

snapshot 是 No.js 的內(nèi)置模塊,第一次執(zhí)行時我們在 snapshot 對象上設(shè)置了一個 hello 屬性,然后再次啟動時輸出該屬性可以看到還存在。下面看看實現(xiàn)原理。No.js 的 C++ 模塊和 JS 模塊都是寫到一個對象中的,大概如下:

const No = {
  buildin: { "tcp": {} },
  libs: { "net": {} },
};

下面是 No.js 啟動時的邏輯。

void No::Core::Run(Environment * env) {
    Isolate * isolate = env->GetIsolate();
    Local<Context> context = env->GetContext();
    // 不是通過快照啟動則創(chuàng)建新的對象,否則復(fù)用快照的對象
    if (!env->has_startup_snapshot()) {
        // 創(chuàng)建新的則設(shè)置到 env,進程退出后 No 會被寫入快照,再次通過快照啟動時可以直接復(fù)用
        env->set_no(Object::New(isolate));
    }
   
    Local<Object> No = env->no();
    // 執(zhí)行 No.js 文件
    func->Call(context, context->Global(), 1, argv).ToLocalChecked();  
    {
        No::MicroTask::MicroTaskScope microTaskScope(env);
    }
    // 啟動事件循環(huán)
    uv_run(env->loop(), UV_RUN_DEFAULT);
    
}

No.js 文件如下。

const {
    loader,
    process,
    snapshot,
} = No.buildin;

// 內(nèi)置 JS 模塊
class NativeModule {
    exports
    constructor(filename) {
        this.filename = filename;
        this.exports = {};
    }
    load() {
        const func = loader.compileNative(this.filename)
        func.call(null, loader.compile, this.exports, this, No);
    }
}

// 加載內(nèi)置 JS 模塊
function require(filename) {
    constmodule = new NativeModule(filename);
    module.load();
    returnmodule.exports;
}

function loaderNativeModules() {
    const modules = [
        {
            filename: 'libs/uv/index.js',
            name: 'uv',
        },
    ];
    No.libs = {};
    for (let i = 0; i < modules.length; i++) {
        const { name, filename } = modules[i];
        No.libs[name] = require(filename);
    }
}
                                       
// 如果通過快照啟動則不需要重新加載內(nèi)置 JS 模塊,直接復(fù)用快照的
if (!snapshot.hasStartupSnapshot()) {
    loaderNativeModules();
}

function runMain() {
    constmodule = require("libs/module/index.js");
    let entry;
    for (let i = 0; i < process.argv.length; i++) {
        if (process.argv[i].endsWith('.js')) {
            entry = process.argv[i];
        }
    }
    if (process.isMainThread) {
        module.load(entry);
    }
}

runMain();

用戶自定義快照

除了可以把 No.js 內(nèi)部的信息寫入快照外,No.js 也支持用戶把自己的信息寫入快照。看下面的一個使用例子。

const snapshot = require("snapshot");

let now = Date.now();

if (snapshot.isBuildSnapshot()) {
    snapshot.addSerialCallback(function() {
        console.log("serial:now=", now);
    });
    snapshot.addDeSerialCallback(function() {
        console.log("deserial:now=", now);
    });
}

addSerialCallback 的函數(shù)參數(shù)會在構(gòu)建快照的過程中被調(diào)用,用戶可以在這里把信息寫入快照,addDeSerialCallback 的函數(shù)參數(shù)會在通過快照啟動時被執(zhí)行,這里可以進行數(shù)據(jù)恢復(fù)。

  1. No --bild_snapshot demo.js 時會輸出:serial:sum=100000000。
  2. No --snapshot_blob snapshot.blob 時會輸出:serial:sum=100000000。 實現(xiàn)原理是通過把一個函數(shù)記錄到快照中,然后通過閉包記錄了之前的上下文,當(dāng)通過快照啟動時恢復(fù)該快照并找到之前的上下文。下面看下實現(xiàn)。
function addSerialCallback(callback, data) {
    serialCallbackQueue.push(new SnapshotTask(callback, data));
    if (serialCallbackQueue.length === 1) {
        snapshot.addSerialCallback(runSerialCallback)
    }
}
```js
addSerialCallback 在 JS 層維護了一個函數(shù)隊列,并設(shè)置 runSerialCallback 到 C++ 層。
```c++
void SetSerializeCallback(V8_ARGS) {
    Environment *env = Environment::GetCurrent(args.GetIsolate());
    // snapshot_serialize_cb 函數(shù)會被寫入快照,在通過快照啟動時被恢復(fù),可以參考之前介紹的內(nèi)容
    env->set_snapshot_serialize_cb(args[0].As<Function>());
}

runSerialCallback 會在進程退出時被執(zhí)行,從而執(zhí)行 addSerialCallback 中的函數(shù)數(shù)組。

{
  No::MicroTask::MicroTaskScope microTaskScope(env);
  No::Core::Run(env);
}
// 用戶代碼執(zhí)行完畢,執(zhí)行 runSerialCallback
env->run_snapshot_serial_callback();
creator.SetDefaultContext(context, v8::SerializeInternalFieldsCallback());
env->serialize(&creator, &snapshot_data);

runSerialCallback 就是遍歷函數(shù)逐個執(zhí)行。

function runSerialCallback() {
    serialCallbackQueue.forEach(task => {
        task.callback()(task.data());
    })
}

addDeSerialCallback 的實現(xiàn)是一樣的,區(qū)別是執(zhí)行時機不同,addDeSerialCallback 的函數(shù)參數(shù)也會被記錄到快照中,在通過快照啟動時被執(zhí)行。

單體應(yīng)用

一般來說,執(zhí)行 JS 運行時需要提供一個入口文件,單體應(yīng)用則不需要,因為它可以在快照中記錄入口函數(shù),從而在通過快照啟動時直接執(zhí)行入口函數(shù),并且可以使用之前的上下文信息。下面是一個例子。

const snapshot = require("snapshot");
// 第一次啟動時計算一個值
let sum = 0;
for (let i = 0; i < 100000000; i++) {
    sum += 1;
}
if (snapshot.isBuildSnapshot()) {
    // 進程退出前執(zhí)行
    snapshot.addSerialCallback(function() {
        console.log("serial:sum=", sum);
    });
    // 通過快照啟動時執(zhí)行
    snapshot.addDeSerialCallback(function() {
        sum++;
        console.log("deserial:sum=", sum);
    });
    // 通過快照啟動時執(zhí)行
    snapshot.setDeserializeMain(function() {
        console.log("setDeserializeMain", sum);
    });
}

執(zhí)行 No --biuld_snapshot demo.js 時 addSerialCallback 的函數(shù)參數(shù)會被執(zhí)行,執(zhí)行 No --snapshot_blob snapshot.blob 時就會執(zhí)行 addDeSerialCallback 函數(shù)參數(shù),最后執(zhí)行 setDeserializeMain 函數(shù)參數(shù)啟動進程。

外部 C++ API

JS 運行時會設(shè)置很多 C++ 層的 API 到 JS 層使用,而這些 C++ API 的函數(shù)地址在不同進程(每次啟動時)是不一樣的,所以我們需要把這些信息告訴 V8。

class ExternalReferenceRegistry {
  public:
  ExternalReferenceRegistry();

const std::vector<intptr_t>& external_references();

bool is_empty() { return external_references_.empty(); }

template <typename T>
void Register(T* address) {
    external_references_.push_back(reinterpret_cast<intptr_t>(address));
  }

private:

bool is_finalized_ = false;
std::vector<intptr_t> external_references_;
};

實現(xiàn)比較簡單,每個注冊到 JS 層的 C++ API 都調(diào) Register 把自己的地址注冊到 ExternalReferenceRegistry 中,在創(chuàng)建快照和通過快照啟動時都傳入順序一樣的函數(shù)地址數(shù)組給 V8 即可,假設(shè)我們有個 C++ API A,第一次啟動構(gòu)建快照時,寫入快照的地址是 0x1,后續(xù)通過快照啟動時 A 的地址變成了 0x2,V8 還通過 0x1 訪問機會報錯,所以我們每次都需要把最新的地址告訴 V8,V8 會重寫函數(shù)對應(yīng)的地址。

  1. 構(gòu)建快照時:
No::ExternalReferenceRegistry external_reference_registry;
const std::vector<intptr_t>& external_references = external_reference_registry.external_references();
v8::SnapshotCreator creator(external_references.data());
Isolate* isolate = creator.GetIsolate();
creator.CreateBlob(...);
  1. 通過快照啟動時:
Isolate::CreateParams create_params;
No::ExternalReferenceRegistry external_reference_registry;
const std::vector<intptr_t>& external_references = external_reference_registry.external_references();
create_params.external_references = external_references.data();

總結(jié)

V8 快照的內(nèi)容還是挺多的,對于 JS 運行時來說需要處理的細節(jié)也比較多,很多地方的代碼可能都需要改造。但是對于需要啟動加速的場景是非常有意義的,比如 serverless,下面是一個啟動加速的例子。

const snapshot = require("snapshot");

if (snapshot.isBuildSnapshot()) {
  global.sum = 0
  for (let i = 0; i < 100000000; i++) {
    global.sum += 1;
  }
} else {
  console.log(global.sum);
}

不通過快照啟動時時間是 0.912501s,通過快照啟動時是 0.00955017s。當(dāng)然這只是個例子,而且通過快照啟動時處理快照也是需要時間的,具體的情況需要具體分析。

以上是最近業(yè)余時間對快照的一些探索(可以參考 https://github.com/theanarkh/nojs),大家如果有興趣的話也可以自行研究,后面會再介紹下 Node.js 中的實現(xiàn)。

責(zé)任編輯:武曉燕 來源: 編程雜技
相關(guān)推薦

2023-10-10 10:23:50

JavaScriptV8

2010-07-20 16:35:52

V8JavaScript瀏覽器

2023-06-05 16:38:51

JavaScript編程語言V8

2022-09-16 08:32:25

JavaC++語言

2020-06-02 13:34:16

支付寶小程序 V8 Worker

2022-04-01 08:02:32

Node.js快照加速hooks

2014-11-26 09:51:24

GithubGoogleV8

2010-08-31 11:42:03

DB2MDC

2022-04-29 08:00:51

V8垃圾回收

2021-05-28 05:30:55

HandleV8代碼

2023-02-28 07:56:07

V8內(nèi)存管理

2023-03-09 09:43:56

架構(gòu)技術(shù)

2011-10-19 13:47:57

ibmdwRationalWAS

2021-08-29 18:34:44

編譯V8C++

2016-10-18 15:18:48

JEECMS V*javaCMS系統(tǒng)

2022-10-24 09:11:05

TypeScriptV8

2022-04-29 08:05:06

內(nèi)存堆外GC

2022-05-06 23:03:48

V8CPUProfiler

2022-08-19 06:40:02

V8GC

2020-08-31 08:11:01

V8 8.5Promise前端
點贊
收藏

51CTO技術(shù)棧公眾號