My friends, my life, my style - James S.F. Hsieh

12/21/2012

JavaScript Object Model

以前學習JavaScript所做的投影片 今天 "是芥末日" 就拿來分享一下.


12/14/2012

Evaluate JS in V8 Engine and WebKit

總是很好奇 WebKit 跟 V8 之間是怎麼運作的, 如何載入一份 JavaScript code 到 HTML DOM 當中. 所幸 chromuim 是完全的 open, 所以可以挖 source code 出來看看. 當 HTML 處理 script tag 時, 首先 WebKit 會先發出 HTTP Request 來提取需要的 JS 檔案. Request 會依照 HTML 中 script tag 出現的順序依序產生. Response 則會因為網路與檔案大小的關係隨機的返回. 不過在一般的狀況下 Evaluate Script 的順序必須跟 script tag 的順序一致. 因為 script 之間可能會有 dependency, 所以必須照順序.

讓我們來看看一個 script 被載入的 timeline


以下是 chromuim 收到 Response 之後  Evaluate Script 的程式碼, Inspector 清楚的指出, JS 檔案必須要完全下載完畢才能開始 Evaluate.

v8::Local ScriptController::compileAndRunScript(const ScriptSourceCode& source)
{
    ASSERT(v8::Context::InContext());

    V8GCController::checkMemoryUsage();

    InspectorInstrumentationCookie cookie = InspectorInstrumentation::willEvaluateScript(m_frame, source.url().isNull() ? String() : source.url().string(), source.startLine());

    v8::Local result;
    {
        // Isolate exceptions that occur when compiling and executing
        // the code. These exceptions should not interfere with
        // javascript code we might evaluate from C++ when returning
        // from here.
        v8::TryCatch tryCatch;
        tryCatch.SetVerbose(true);

        // Compile the script.
        v8::Handle code = deprecatedV8String(source.source());
#if PLATFORM(CHROMIUM)
        TRACE_EVENT_BEGIN0("v8", "v8.compile");
#endif
        OwnPtr scriptData = ScriptSourceCode::precompileScript(code, source.cachedScript());

        // NOTE: For compatibility with WebCore, ScriptSourceCode's line starts at
        // 1, whereas v8 starts at 0.
        v8::Handle script = ScriptSourceCode::compileScript(code, source.url(), source.startPosition(), scriptData.get());
#if PLATFORM(CHROMIUM)
        TRACE_EVENT_END0("v8", "v8.compile");
        TRACE_EVENT0("v8", "v8.run");
#endif

        // Keep Frame (and therefore ScriptController) alive.
        RefPtr protect(m_frame);
        result = ScriptRunner::runCompiledScript(script, m_frame->document());
        ASSERT(!tryCatch.HasCaught() || result.IsEmpty());
    }

    InspectorInstrumentation::didEvaluateScript(cookie);

    return result;
}
ScriptController::compileAndRunScript 可以被分成三個部分
  1. Precompile script: Precompile 主要是產生 Compile 所需要的 metadata 諸如: function entries 與symbols table. 細節可以參考 PreParser::ParseSourceElements 與 PreParser::ParseSourceElement
  2. Compile script: MakeFunctionInfo 可以在分成兩個階段
    • Create Abstract syntax tree: Parser::ParseSourceElements 主要是分析 JS 的語法並產生出語法樹.
    • Gen Machine Code: FullCodeGenerator::Generate 我們可以發現 V8 並沒有 interpreter 的方式去執行JS 而是完全使用 JIT 的方式執行 machine code. 而且也沒有產生 Intermediate code (Byte code) 而是直接由 AST 產生出 Machine code. 這跟其他的 JS interpreter 或是 JAVA 有很大的不同. 一個 JS 檔案對應會產生一個  function.
  3. Run compiled script: 最後的步驟是執行這個 function. 所參考到的 ScriptExecutionContext 就是 DOM 的 window.
一般我們會認為這三個階段最花時間的會是 Compile script. 因為 Compile script 包含了語法分析跟 Machine code 的產生. 以 Evaluate http://code.jquery.com/jquery-1.8.3.min.js 為例在 Android browser 上 Evaluate 花費的時間如下:

Precompile script: 0.640928 ms, 0.183122 ms, 15.534869 ms, 12.910118 ms, 13.093239 ms
Compile script: 0.152602 ms, 0.549367 ms, 0.091561 ms, 0.061041 ms, 0.183123 ms
Run compiled script: 173.874561 ms, 153.364871 ms, 175.064856 ms, 120.647032 ms, 114.512437 ms

結果 Run compiled script 花費超過 90% 的時間. 因為本質上 script language 跟 non-script language 有很大的不同, JS 是種 prototype based language, 組裝 prototype 就好比 non-script language 在定義 class. 然而 non-script language 組裝 class 所花費的時間在 build time, 而 JS 則是在 run-time 也就是 Run compiled script 這個時機. 所以 Run compiled script 自然會花較多的時間.

對於一個 Web 來說 Evaluate JS 所花費數百 ms 的時間通常都比 HTTP round-trip time 來的小得多(如果 JS 沒有 local cache). 但是, 對於一個 WebApp 而言, 這數百 ms 的等待就非常的有感覺. 這會引響 app cold start 所等待的時間. 就以 Android 來說 一個 Activity 的啟動並且包含 WebView 初始化的時間可能就需要 2 秒, 再加上 Evaluate JS 就將近需要三秒鐘的時間. 能減少等待時間就變得很重要. 以下有多個方法來減少 Evaluate JS 的時間:

Delay load JS

將 JS 分離出哪些可以 delay load JS 或是 on-demand load JS, 將不是立即需要的部分抽離出來可以減少 cold start 過程中 Evaluate JS 的時間. 直到需要的時候再利用 script tag 來載入需要的 JS. 通常這是最有效的方法.

loadJS = function(jsPaths, callback) {
 var count = jsPaths.length;
 $(jsPaths).each(function(index, jsPath) {
  this._script = script = document.createElement('script');
  this._script.src = jsPath;
  this._script.type = 'text/javascript';
  this._script.onload = function() {
   if(--count == 0 && callback) callback();
  };

  document.getElementsByTagName('head')[0].appendChild(this._script); 
 });
};
loadJS([jsPath1, jsPath2], function() {
  /* do something when the JS files are loaded.
});

Google Closure Compiler

一般我們會使用 JSMin 或是 YUI Compressor 減小 JS 檔案的體積, 這些 utility 通常是利用簡化 JS expression, 刪除非必要空白, 與減短區域變數的名稱等方式來縮小 JS 的大小以減少 download 的時間. 我相信這對 Evaluate JS 的前兩個步驟 Precompile script 與 Compile script 應該有些許的幫助.   不過這兩個部分所佔的時間其實不多. Closure Compiler 與這些 utility 的方法就有些不同了,  它會分析 JS 的執行路徑, 並且簡化 JS 的執行方式. 這可以有效減少 Run compiled script 所需要的時間. 以我的經驗, Closure Compiler 可以減少大約 20% 左右的時間.

Customized JS

把沒有使用或是沒有必要的程式碼拔除是個很有效的對策, 通常 3th party library 都有提供自動化的 Builder 來產生 Customized build. 以下列出幾個例子


這些 Builder 都提供模組化的方式讓你產生 Customized build 來簡化 JS 的大小跟複雜度.
當然, 你也可以自己 review 一下這些 JS, 並且把你確定沒用到的部分拔除或減化. 舉個例子: jQuery 為了達到跨平台. 會使用 jQuery.support 與多個 assert 來測試 browser 對於 DOM 操作支援的能力. 但這對於一個 WebApp 就不一定必要, 因為 WebApp 通常會執行在平台上特定的 WebView 中, 所以我們可以事先知道這些 WebView 的能力來簡化 jQuery.support 與 assert. 這樣也可以明顯的減少 Run compiled script 所花費的時間.


最後, 總是覺得 virtual machine 跟 interpreter 是個很酷很神祕的東西, 如何轉譯 byte code 有效率的產生各個平台的 machine code, 如何追蹤記憶體的使用做到 garbage collection, 如何做到 managed code 跟 native code 的 interoperability. 而 V8 真的是個很棒的主題用來接露這些設計上的細節, 真的值得花時間來研究. Reference: