- 原文地址:Oxidizing Source Maps with Rust and WebAssembly
- 原文做者:Nick Fitzgerald
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:D-kylin
Tom Tromey 和我嘗試使用 Rust 語言進行編碼,而後用 WebAssembly 進行編譯打包後替換 source-map
(源碼地址索引,如下行文爲了理解方便均不進行翻譯)的 JavaScript 工具庫中性能敏感的部分。在實際場景中以相同的基準進行對比操做,WebAssembly 的性能要比已有的 source-map 庫快上 5.89 倍。另外,屢次測試結果也更爲一致:相對一致的狀況下誤差值很小。前端
咱們以提升性能的名義將那些使人費解又難以閱讀的 JavaScript 代碼替換成更加語義化的 Rust 代碼,這確實行之有效。android
如今,咱們把 Rust 結合 WebAssembly 使用的經驗分享給你們,也鼓勵程序員按照本身的需求對性能敏感的 JavaScript 進行重構。ios
source map 文件提供了 JavaScript 源碼被編譯器[0]、壓縮工具、包管理工具轉譯成的文件之間的地址索引供編程人員使用。JavaScript 開發者工具使用 source-map 後能夠實現字符級別的回溯,調試工具中的按步調試也是依賴它來實現的。Source-map 對報錯信息的編碼方式與 DWARF’s .debug_line
的部分標準很類似。git
source-map 對象是 JSON 對象的其中一個分支。其中 「映射集」
用字符串表示,是 source-map 的重要組成部分,包含了最終代碼和定位對象的雙向索引。程序員
咱們用 extended Backus-Naur form (EBNF) 標準描述 「映射集」
的字符串語法。angularjs
Mappings 是 JavaScript 代碼塊的分組行號,每個映射集只要以分號結尾了就表明一個獨立的映射集,它就自增 1。同一行 JavaScript 代碼若是生成多個映射集,就用逗號分隔開:github
<mappings> = [ <generated-line> ] ';' <mappings>
| ''
;
<generated-line> = <mapping>
| <mapping> ',' <generated-line>
;
複製代碼
每個獨立的映射集都能定位到當初生成它的那段 JavaScript 代碼,還能有一個關聯名字的可選項能定位到那段代碼中的源碼字符串:web
<mapping> = <generated-column> [ <source> <original-line> <original-column> [ <name> ] ] ;
複製代碼
每一個映射集組件都經過一種叫作大數值的位數可變表示法(Variable Length Quantity,縮寫爲 VLQ)編碼成二進制數字。文件名和相關聯的名字被編碼後儲存在 source-map 的 JSON 對象中。每個值標註了源碼最後出現的位置,如今,給你一個 <source>
值那麼它跟前一個 <source>
值就給咱們提供了一些信息。若是這些值之間趨向於愈來愈小,就說明它們在被編碼的時候更加緊密:算法
<generated-column> = <vlq> ;
<source> = <vlq> ;
<original-line> = <vlq> ;
<original-column> = <vlq> ;
<name> = <vlq> ;
複製代碼
利用 VLQ 編碼後的字符都能從 ASCII 字符集中找到,好比大小寫的字母,又或者是十進制數字跟一些符號。每一個字符都表示了一個 6 位大小的值。VLQ 編碼後的二進制數前五位用來表示數值,最後一位只用來作標記正負。shell
與其向你解釋 EBNF 標準,不如來看一段簡單的 VLQ 轉換代碼實現:
constant SHIFT = 5
constant CONTINUATION_BIT = 1 << SHIFT
constant MASK = (1 << SHIFT) - 1
decode_vlq(input):
let accumulation = 0
let shift = 0
let next_digit_part_of_this_number = true;
while next_digit_part_of_this_number:
let [character, ...rest] = input
let digit = decode_base_64(character)
accumulation += (digit & MASK) << shift
shift += SHIFT;
next_digit_part_of_this_number = (digit & CONTINUATION_BIT) != 0
input = rest
let is_negative = accumulation & 1
let value = accumulation >> 1
if is_negative:
return (-value, input)
else:
return (value, input)
複製代碼
source-map
JavaScript 工具庫source-map
是由 火狐開發者工具團隊 維護,發佈在 npm 上。它是 JavaScript 社區最流行的依賴包之一,下載量達到 每週 1000 萬次。
就像許多軟件項目同樣,source-map
工具庫最開始也沒有很好的去實現它,以致於後面只能經過不斷的修復來改善性能。截止到本文完成以前,其實已經有了不錯的性能表現了。
當咱們使用 source-map,很大一部分的時間都是消耗在解析 「映射集」
字符串和構建數組對:一旦 JavaScript 的定位改變了,另外一個文件的代碼標示的定位也要改變。選用合適的二進制查找方式對數組進行查找。解析和排序操做只有在特定的時機纔會被調用。例如,在調試工具中查看源碼時,不須要對任何的映射集進行解析和排序。一次性的解析和排序、查找並不會成爲性能瓶頸。
VLQ 編碼函數經過輸入字符串,解析字符串並返回一對由解析結果和其他輸入組成的值。一般把函數的返回值寫成有兩個屬性組成的 對象
,這樣更具備可讀性,也方便往後進行格式轉換。
function decodeVlqBefore(input) {
// ...
return { result, rest };
}
複製代碼
咱們發現返回這樣的對象成本很高。針對 JavaScript 的即時編譯(Just-In-Time,JIT)優化,很難用第三方編譯的方式來優化這部分花銷。由於 VLQ 的編碼事件老是頻繁產生,因此這部分的內存分配工做給垃圾收集機制帶來很大的壓力,致使垃圾收集工做就像是走走停停同樣。
爲了禁用內存分配,咱們 修改程序 的第二個參數:將返回 對象
進行變體並做爲輸出參數,這樣就把結果當成一個外部 對象
的屬性。咱們能夠確定這個外部對象與 VLQ 函數返回的對象是一致的。雖然損失了一點可讀性,可是執行效率更高:
function decodeVlqAfter(input, out) {
// ...
out.result = result;
out.rest = rest;
}
複製代碼
當查找一個位置長度的字符串或者 base 64 字符,VLQ 編碼函數會 拋出
一個 報錯
。咱們發現若是 若是轉換 base 64 數字出現錯誤,編碼函數返回 -1
而不是 拋出
一個 報錯
,那麼 JavaScript 的即時編譯效率更高。雖然損失了一點可讀性,可是執行效率又高了那麼一丟丟。
剖析 SpiderMonkey 引擎中 JITCoach 原型,咱們發現 SpiderMonkey 引擎即時編譯機制是使用多態短路徑實時緩存 對象
的 getter 和 setter。它的即時編譯沒有如咱們期待的那樣直接經過快速訪問獲得對象的屬性,由於以一樣的 「形狀」 (或者稱之爲 「隱藏類」) 是訪問不到它返回出來的對象。有一些屬性可能都不是你存入對象時的鍵名,甚至鍵名是徹底省略掉的,好比當它在映射集中定位不到名字時。建立一個 Mapping 類生成器,初始化每個屬性,咱們配合即時編譯,爲 Mapping 類添加通用屬性。完整結果能夠在這裏看到 另外一種性能改進:
function Mapping() {
this.generatedLine = 0;
this.generatedColumn = 0;
this.lastGeneratedColumn = null;
this.source = null;
this.originalLine = null;
this.originalColumn = null;
this.name = null;
}
複製代碼
對兩個映射集數組進行排序時,咱們使用自定義對比函數。當 source-map
工具庫源碼被第一次寫入,SpiderMonkey 的 Array.prototype.sort
是用 C++ 實現來提高性能[1]。儘管如此,當使用外部提供的對比函數並對一個巨大的數組進行 排序
的時候,排序代碼也須要調用不少次對比函數。從 C++ 中調用 JavaScript 相對來講也是很昂貴的花銷,因此調用自定義對比函數會使得排序性能急速降低。
基於上述條件,咱們 實現了另外一個版本 Javascript 快排。它只能經過 C++ 調用 Javascript 時才能使用,它也容許 JavaScript 即時編譯時做爲排序函數的對比函數傳入,用來獲取更好的性能。這個改進給咱們帶來大幅度的性能提高,同時只須要損失很小的代碼可讀性。
WebAssembly 是一種新的技術,它以二進制形式運行在 Web 瀏覽器底層,爲瀏覽器隔離危險代碼和減小代碼量所設計的。如今已經做爲 Web 的標準,並且大多數的瀏覽器廠商已經支持這個功能。
WebAssembly 開闢一塊新的棧區供機器運行,有現代處理器架構的支持能更好的處理映射,它能夠直接操做一大塊連續的儲存 buffer 字節。WebAssembly 不支持自動化的垃圾回收,不過 在不久的未來 它也會繼承 JavaScript 對象的垃圾回收機制。控制流是具備結構化的,比起在代碼間隨意的打標記或者跳躍,它被設計用來提供一種更可靠、運行一致的執行流程。處理一些架構上的邊緣問題,好比:超出表示範圍的數值怎麼截取、溢出問題、規範 NaN
。
WebAssembly 的目標是得到或者逼近原始指令的運行速度。目前在大多數的基準測試中跟原始指令相比 只相差 1.5x 了。
由於缺少垃圾收集器,要編譯成 WebAssembly 語言僅限那些沒有運行時和垃圾採集器的編程語言,除非把控制器和運行時也編譯成 WebAssembly。實際中這些通常很難作到。如今,語言開發者事實上是把 C,C++ 和 Rust 編譯成 WebAssembly。
Rust 是一種更加安全和高效的系統編程語言。它的內存管理更加安全,不依賴於垃圾回收機制,而是容許你經過靜態追蹤函數 ownership 和 borrowing 這兩個方法來申請和釋放內存。
使用 Rust 來編譯成 WebAssembly 是一種不錯的選擇。因爲語言設計者一開始就沒有爲 Rust 設計垃圾自動回收機制,也就不用爲了編譯成 WebAssembly 作額外的工做。Web 開發者還發現一些在 C 和 C++ 沒有的優勢:
rustup
,cargo
和 crates.io 的完整生態系統。這是 C 和 C++ 所不能比擬的。迭代算法
中不斷產生內存碎片。Rust 則能夠在編譯時就避免大部分相似的性能陷阱。當咱們決定把 source-map 中使用頻率最高的解析和查找功能進行重構,就須要考慮到 JavaScript 和 WebAssembly 的運行邊界問題。若是出現了 JavaScript 即時編譯和 WebAssembly 相互穿插運行可能會影響彼此原來的執行效率。關於這個問題能夠回憶一下前面咱們討論過的在 C++ 代碼中調用 JavaScript 代碼的例子[2]。因此肯定好邊界來最小化兩個不一樣語言相互穿插執行的次數顯得尤其重要。
在 VLQ 編碼函數中供選擇的 JavaScript 和 WebAssembly 的運行邊界其實不多。VLQ 編碼函數對 「映射集」
字符串的每一次 Mapping 時須要被引用 1~4 次,在整個解析過程不得不在 JavaScript 和 WebAssembly 的邊界來回切換不少次。
所以,咱們決定只用 Rust/WebAssembly 解析整個 「映射集」
字符串,而後把解析結果保留在內存中,WebAssembly 堆就能夠直接查找到解析後的數據。這意味着咱們不用把數據從 WebAssembly 堆中複製出來,也就不須要頻繁的在 JavaScript 和 WebAssembly 邊界來回切換了。除此以外,每次的查找只須要切換一次邊界,每執行一次 Mapping 只不過是在解析結果中多查找一次。每次查找只產生一個結果,而這樣的操做次數屈指可數。
經過這兩個單元化測試,咱們確信利用 Rust 語言來實現是正確的。一個是 source-map
工具庫已有的單元測試,另外一個是 快速查找
性能的單元測試。這個測試的是經過解析隨機輸入 「映射集」
字符串,判斷執行結果的多個性能指標。
咱們基於 Rust 實現 crates.io,利用 crates.io 的 api 做爲 Mapping 函數對 「映射集」
進行解析和查找。
對 source-map 進行 Mapping 的第一步是 VLQ 編碼。這裏是咱們實現的 vlq
工具庫,基於 Rust 實現,發佈到 crates.io 上。
decode64
函數解碼結果是一個 base 64 數值。它使用匹配模式和可讀性良好的 Result
—— 處理錯誤。
Result<T, E>
函數運行獲得一個類型爲 T
,值爲 V
就返回 Ok(v)
;運行獲得一個類型爲 E
,值爲 error
就返回 Err(error)
來提供報錯細節。decode64
函數運行獲得一個類型爲 Result<u8, Error>
的返回值,若是成功,值爲 u8
,若是失敗,值爲 vlq::Error
:
fn decode64(input: u8) -> Result<u8, Error> {
match input {
b'A'...b'Z' => Ok(input - b'A'),
b'a'...b'z' => Ok(input - b'a' + 26),
b'0'...b'9' => Ok(input - b'0' + 52),
b'+' => Ok(62),
b'/' => Ok(63),
_ => Err(Error::InvalidBase64(input)),
}
}
複製代碼
經過 decode64
函數,咱們能夠對 VLQ 值進行解碼。decode
函數將可變引用做爲輸入字節的迭代器,消耗須要解碼的 VLQ,最後返回 Result
函數做爲解碼結果。
pub fn decode<B>(input: &mut B) -> Result<i64>
where
B: Iterator<Item = u8>,
{
let mut accum: u64 = 0;
let mut shift = 0;
let mut keep_going = true;
while keep_going {
let byte = input.next().ok_or(Error::UnexpectedEof)?;
let digit = decode64(byte)?;
keep_going = (digit & CONTINUED) != 0;
let digit_value = ((digit & MASK) as u64)
.checked_shl(shift as u32)
.ok_or(Error::Overflow)?;
accum = accum.checked_add(digit_value).ok_or(Error::Overflow)?;
shift += SHIFT;
}
let abs_value = accum / 2;
if abs_value > (i64::MAX as u64) {
return Err(Error::Overflow);
}
// The low bit holds the sign.
if (accum & 1) != 0 {
Ok(-(abs_value as i64))
} else {
Ok(abs_value as i64)
}
}
複製代碼
不像被替換掉的 JavaScript,這段代碼沒有爲了性能而下降錯誤處理代碼的可讀性,可讀性更好的錯誤處理執行邏輯更容易理解,也沒有涉及到堆的值包裝和棧的壓棧出棧。
"mappings"
字符串咱們開始定義一些輔助函數。is_mapping_separator
函數判斷給定的數據可否被 Mapping
若是能夠就返回 true
,不然返回 false
。這是一個語法與 JavaScript 很類似的函數:
#[inline]
fn is_mapping_separator(byte: u8) -> bool {
byte == b';' || byte == b','
}
複製代碼
而後咱們定義一個輔助函數用來讀取 VLQ 數據並把它添加到前一個值中。這個函數無法用 JavaScript 類比了,每讀取一段 VLQ 數據就要運行這個函數一遍。Rust 能夠控制參數在內存中以怎樣的形式存儲,JavaScript 則沒有這個功能。雖然咱們能夠用一組數字屬性引用 Object
或者把數字變量經過閉包保存下來,可是依然模擬不了 Rust 在引用一組數組屬性的時候作到零花銷。JavaScript 只要運行時就必定會有相關的時間花銷。
#[inline]
fn read_relative_vlq<B>(
previous: &mut u32,
input: &mut B,
) -> Result<(), Error>
where
B: Iterator<Item = u8>,
{
let decoded = vlq::decode(input)?;
let (new, overflowed) = (*previous as i64).overflowing_add(decoded);
if overflowed || new > (u32::MAX as i64) {
return Err(Error::UnexpectedlyBigNumber);
}
if new < 0 {
return Err(Error::UnexpectedNegativeNumber);
}
*previous = new as u32;
Ok(())
}
複製代碼
總而言之,基於 Rust 實現的 「映射集」
解析與被替換調的 JavaScript 實現語法邏輯很是類似。儘管如此,使用 Rust 咱們能夠控制底層哪些功能要打包到一塊兒,哪些用輔助函數來解決。JavaScript 語言對底層的控制權就小了不少,舉個簡單例子,解析映射 對象
只能用 JavaScript 原生方法。Rust 語言的優點源於把內存的分配和垃圾回收交給編程人員本身去實現:
pub fn parse_mappings(input: &[u8]) -> Result<Mappings, Error> {
let mut generated_line = 0;
let mut generated_column = 0;
let mut original_line = 0;
let mut original_column = 0;
let mut source = 0;
let mut name = 0;
let mut mappings = Mappings::default();
let mut by_generated = vec![];
let mut input = input.iter().cloned().peekable();
while let Some(byte) = input.peek().cloned() {
match byte {
b';' => {
generated_line += 1;
generated_column = 0;
input.next().unwrap();
}
b',' => {
input.next().unwrap();
}
_ => {
let mut mapping = Mapping::default();
mapping.generated_line = generated_line;
read_relative_vlq(&mut generated_column, &mut input)?;
mapping.generated_column = generated_column as u32;
let next_is_sep = input.peek()
.cloned()
.map_or(true, is_mapping_separator);
mapping.original = if next_is_sep {
None
} else {
read_relative_vlq(&mut source, &mut input)?;
read_relative_vlq(&mut original_line, &mut input)?;
read_relative_vlq(&mut original_column, &mut input)?;
let next_is_sep = input.peek()
.cloned()
.map_or(true, is_mapping_separator);
let name = if next_is_sep {
None
} else {
read_relative_vlq(&mut name, &mut input)?;
Some(name)
};
Some(OriginalLocation {
source,
original_line,
original_column,
name,
})
};
by_generated.push(mapping);
}
}
}
quick_sort::<comparators::ByGeneratedLocation, _>(&mut by_generated);
mappings.by_generated = by_generated;
Ok(mappings)
}
複製代碼
最後,咱們仍然在 Rust 代碼中使用咱們本身定義的快排,這多是全部 Rust 代碼中可讀性最差了。咱們還發現,在原生代碼環境中,標準庫的內置排序函數執行效率更高,可是一旦把運行環境換成 WebAssembly,咱們定義的排序函數比標準庫的內置排序函數執行效率更高。(對於這樣的差別很意外,不過咱們也沒有再深究了。)
WebAssembly 的對外函數接口(foreign function interface,簡稱 FFI)受限於標量值,因此一些以 Rust 語言編寫,經過 WebAssembly 轉成 JavaScript 代碼後的函數參數只能是標量數值類型,返回值也是標量數值類型。所以,JavaScript 要求 Rust 爲 「映射集」
字符串分配一塊緩衝區並返回該 buffer 字節的地址指針。而後,JavaScript 必須複製出 「映射集」
字符串的 buffer 字節,這時候由於 FFI 的限制什麼也作不了,只能把整段連續的 WebAssembly 內存直接寫入。以後 JavaScript 調用 parse_mappings
函數進行 buffer 字節的初始化工做,初始化完畢後返回解析結果的指針。完成上述這些前置工做後,JavaScript 就可使用 WebAssembly 的 API ,給定一些數值查找結果,或者給定一個指針獲得解析後的映射集。全部查詢結果完畢之後,JavaScript 會告訴 WebAssembly 釋放存儲映射集結果的內存空間。
全部的暴露出去的 WebAssembly APIs 都被封裝在一個 「小膠箱」 裏。這樣的分離頗有用,它容許咱們用測試環境來執行 source-map-mappings
。若是你想編譯成純的 WebAssembly 代碼也能夠,只須要把編譯環境修改爲 WebAssembly。
另外,受限於 FFI 的傳值要求,那麼輸出的函數必須知足一下兩點:
#[無名]
屬性,要方便 JavaScript 能調用它 。外部 "C"
以便提取到 .wasm
公共文件中。不一樣於核心庫,這些代碼暴露功能給 WebAssembly 轉 JavaScript,有必要提醒你,頻繁使用很是的 不安全
。 只要調用 外部
函數和使用指針從 FFI 邊界接收指針,就是 不安全
,由於 Rust 編譯器無法校驗另外一端是否安全。咱們不多關心到這個安全性問題 —— 最壞的狀況下咱們能夠作一個 陷阱
(把 JavaScript 端的 報錯
所有抓住),或者直接返回一個報錯響應。在同一段地址中,能夠向地址寫入內容要比只是將地址儲存的內容以二進制字節運行要危險的多,若是可寫入的話,攻擊者就能夠欺騙程序跳轉到特定的內存地址,而後插入一段他本身的 shell 腳本代碼。
咱們輸出的一個最簡單是函數功能是把工具庫產生的一個報錯捕獲到。它提供了 libc
中 errno
相似的功能,它會將 API 運行出錯時報告 JavaScript 究竟是什麼樣的錯誤。咱們老是把最近的報錯保留在全局對象上,這個函數能夠檢索錯誤值:
static mut LAST_ERROR: Option<Error> = None;
#[no_mangle]
pub extern "C" fn get_last_error() -> u32 {
unsafe {
match LAST_ERROR {
None => 0,
Some(e) => e as u32,
}
}
}
複製代碼
JavaScript 和 Rust 的第一次交互發生在爲 buffer 字節分配內存空間來存儲 「映射集」
字符串。咱們但願能有一塊獨立的,由 u8
組成的連續塊,它建議使用 Vec<u8>
,但咱們想要暴露一個簡單的指針給 JavaScript。一個簡單的指針能夠跨越 FFI 的邊界,可是很容易在 JavaScript 端引發報錯。咱們能夠用 Box<Vec<u8>>
添加一個鏈接層或者保存在外部數據中,另外一端有須要這份數據的時候再載體進行格式化。咱們決定採用後一個方法。
這個載體由如下三者組成:
當咱們暴露一個堆內存元素的指針給 JavaScript,咱們須要一種方式來保存長度和容量,未來經過 Vec
重建它。咱們在堆元素的開頭添加兩個額外的詞來存儲長度和容量,而後咱們把這個添加了兩個標註的指針傳給 JavaScript:
#[no_mangle]
pub extern "C" fn allocate_mappings(size: usize) -> *mut u8 {
// Make sure that we don't lose any bytes from size in the remainder. let size_in_units_of_usize = (size + mem::size_of::<usize>() - 1) / mem::size_of::<usize>(); // Make room for two additional `usize`s: we'll stuff capacity and
// length in there.
let mut vec: Vec<usize> = Vec::with_capacity(size_in_units_of_usize + 2);
// And do the stuffing.
let capacity = vec.capacity();
vec.push(capacity);
vec.push(size);
// Leak the vec's elements and get a pointer to them. let ptr = vec.as_mut_ptr(); debug_assert!(!ptr.is_null()); mem::forget(vec); // Advance the pointer past our stuffed data and return it to JS, // so that JS can write the mappings string into it. let ptr = ptr.wrapping_offset(2) as *mut u8; assert_pointer_is_word_aligned(ptr); ptr } 複製代碼
把 buffer 字節初始化爲 「字符集」
字符串以後,JavaScript 把 buffer 字節的控制器交給 parse_mappings
,將字符串解析爲可查找結構。解析成功會返回 Mappings
後的結構,失敗就返回 NULL
。
parse_mappings
要作的第一步就是恢復 Vec
的長度和容量。第二部,「映射集」
字符串數據被截取,在被截取的整個生命週期內都沒法從當前做用域檢測到,只有當他們被從新分配到內存中,並被咱們的工具庫解析爲 「字符集」
字符串以後才能獲取到。不論解析結果有沒有成功,咱們都從新申請 buffer 字節來儲存 「字符集」
字符串,而後返回一個指針指向解析成功的結果,或者返回一個指針指向 NULL
。
/// 留意在匹配的生命週期內做用域中的引用,
/// 某些 `不安全` 的操做,好比解除指針關聯引用。
/// 生命週期內返回一些不保留的引用,
/// 使用這個函數保證咱們不會一不當心的使用了
/// 一個非法的引用值。
#[inline]
fn constrain<'a, T>(_scope: &'a (), reference: &'a T) -> &'a T
where
T: ?Sized
{
reference
}
#[no_mangle]
pub extern "C" fn parse_mappings(mappings: *mut u8) -> *mut Mappings {
assert_pointer_is_word_aligned(mappings);
let mappings = mappings as *mut usize;
// 在指針指向映射集字符串前將數據拿出
// string.
let capacity_ptr = mappings.wrapping_offset(-2);
debug_assert!(!capacity_ptr.is_null());
let capacity = unsafe { *capacity_ptr };
let size_ptr = mappings.wrapping_offset(-1);
debug_assert!(!size_ptr.is_null());
let size = unsafe { *size_ptr };
// 從指針的截取片斷構造一個指針並解析成映射集。
let result = unsafe {
let input = slice::from_raw_parts(mappings as *const u8, size);
let this_scope = ();
let input = constrain(&this_scope, input);
source_map_mappings::parse_mappings(input)
};
// 從新分配映射集字符串的內存並添加兩個前置的數據。
let size_in_usizes = (size + mem::size_of::<usize>() - 1) / mem::size_of::<usize>();
unsafe {
Vec::<usize>::from_raw_parts(capacity_ptr, size_in_usizes + 2, capacity);
}
// 返回結果,保存一些報錯給另外一端語言提供幫助
// 若是 JavaScript 須要的話。
match result {
Ok(mappings) => Box::into_raw(Box::new(mappings)),
Err(e) => {
unsafe {
LAST_ERROR = Some(e);
}
ptr::null_mut()
}
}
}
複製代碼
當咱們進行查找時,咱們須要找一個方法來轉換結果,才能傳給 FFI 使用。查找結果多是一個 映射
或者集合組成的 映射
,映射
不能直接給 FFI 使用,除非咱們進行封裝。咱們確定不但願對 映射
進行封裝,由於以後咱們還可能須要從原來的結構中獲取內容,那時咱們還要費時費力的分配內存和間接取值。咱們的方法是調用一個引導進來的函數處理每個 映射
。
mappings_callback
就是一個 外部
函數,它不是本地定義的函數,而是在 WebAssembly 模塊實例化的時候由 JavaScript 引導進來。mappings_callback
將 映射
分解成不一樣的部分:每一個文件都是被展平後的 映射
,被轉換後能夠做爲參數傳遞給 FFI 使用。可選項 <T>
咱們加入一個 bool
參數控制不一樣的轉換結果,由 可選項 <T>
是 Some
仍是 None
決定參數 T
是合法值仍是無用值:
extern "C" {
fn mapping_callback(
// These two parameters are always valid.
generated_line: u32,
generated_column: u32,
// The `last_generated_column` parameter is only valid if
// `has_last_generated_column` is `true`.
has_last_generated_column: bool,
last_generated_column: u32,
// The `source`, `original_line`, and `original_column`
// parameters are only valid if `has_original` is `true`.
has_original: bool,
source: u32,
original_line: u32,
original_column: u32,
// The `name` parameter is only valid if `has_name` is `true`.
has_name: bool,
name: u32,
);
}
#[inline]
unsafe fn invoke_mapping_callback(mapping: &Mapping) {
let generated_line = mapping.generated_line;
let generated_column = mapping.generated_column;
let (
has_last_generated_column,
last_generated_column,
) = if let Some(last_generated_column) = mapping.last_generated_column {
(true, last_generated_column)
} else {
(false, 0)
};
let (
has_original,
source,
original_line,
original_column,
has_name,
name,
) = if let Some(original) = mapping.original.as_ref() {
let (
has_name,
name,
) = if let Some(name) = original.name {
(true, name)
} else {
(false, 0)
};
(
true,
original.source,
original.original_line,
original.original_column,
has_name,
name,
)
} else {
(
false,
0,
0,
0,
false,
0,
)
};
mapping_callback(
generated_line,
generated_column,
has_last_generated_column,
last_generated_column,
has_original,
source,
original_line,
original_column,
has_name,
name,
);
}
複製代碼
全部輸出的查找函數都有類似的結構。它們一開始都是轉換 *mut Mappings
成一個 &mut Mappings
引用。&mut Mappings
生命週期僅限於當前範圍,以強制它只用於這個函數的調用,在它被從新分配內存後不能再使用。其次,每個查找方法都依賴於 Mapping
方法。每一個被輸出的函數都調用 mapping_callback
的結果都是 映射
。
輸出一個典型的查找函數 all_generated_locations_for
,它包裹了Mappings::all_generated_locations_for
方法,並找到全部源標註的映射依賴:
#[inline]
unsafe fn mappings_mut<'a>( _scope: &'a (),
mappings: *mut Mappings,
) -> &'a mut Mappings { mappings.as_mut().unwrap() } #[no_mangle] pub extern "C" fn all_generated_locations_for( mappings: *mut Mappings, source: u32, original_line: u32, has_original_column: bool, original_column: u32, ) { let this_scope = (); let mappings = unsafe { mappings_mut(&this_scope, mappings) }; let original_column = if has_original_column { Some(original_column) } else { None }; let results = mappings.all_generated_locations_for( source, original_line, original_column, ); for m in results { unsafe { invoke_mapping_callback(m); } } } 複製代碼
最後,當 JavaScript 完成查找 映射集
時,必須輸出 free_mappings
函數來爲結果從新分配內存:
#[no_mangle]
pub extern "C" fn free_mappings(mappings: *mut Mappings) {
unsafe {
Box::from_raw(mappings);
}
}
複製代碼
.wasm
文件爲目標添加 wasm32-unknown-unknown
給 Rust 編譯成 WebAssembly 帶來可能,並且 rustup
使得安裝 Rust 的編譯工具指向 wasm32-unknown-unknown
更加便捷:
$ rustup update
$ rustup target add wasm32-unknown-unknown
複製代碼
如今咱們就有了一個 wasm32-unknown-unknown
編譯器, 經過修改 --target
標記就能夠實現不一樣的語言到 WebAssembly 之間的編譯:
$ cargo build --release --target wasm32-unknown-unknown
複製代碼
.wasm
後綴的編譯文件保存在 target/wasm32-unknown-unknown/release/source_map_mappings_wasm_api.wasm
。
儘管咱們已經有一個能夠運行的 .wasm
文件,工做還沒完成:這個 .wasm
文件體積仍然太大了。生產環境的 .wasm
文件體積越小越好,咱們經過如下工具一步步壓縮它:
wasm-gc
,--gc-sections
標記了要移除沒有使用過的對象文件,對於 .wasm
文件,ELF,Mach-O 除外。它會找到哪些輸出函數沒有被用過,而後從 .wasm
文件中移除。
wasm-snip
,用 非訪問性
的指令來替代 WebAssembly 的函數體,這對於那些運行時從頭至尾沒有沒調用過,可是 wasm-gc
靜態分析無法移除掉,經過手動配置編譯結果。丟棄一個函數引用指針使得其餘函數無法訪問到失去引用指針的函數,因此頗有必要在此操做以後再一次使用 wasm-gc
。
wasm-opt
,用 binaryen
優化 .wasm
文件,壓縮文件體積並提升運行時的性能。實際上,隨着後端底層虛擬機愈來愈成熟,這步操做變得無關緊要。
咱們的 生產流程配置 是 wasm-gc
→ wasm-snip
→ wasm-gc
→ wasm-opt
。
在 JavaScript 使用 WebAssembly 的首要問題就是,如何加載 .wasm
文件。 source-map
工具庫的運行環境主要有三個:
不一樣的環境使用不一樣的方式將 .wasm
文件加載爲 ArrayBuffer
字節,才能在 JavaScript 運行時進行編譯使用。在網頁和火狐瀏覽器裏能夠用標準化的 fetch
API 創建 HTTP 請求來加載 .wasm
文件。它是一個工具庫,負責將 URL 指向須要從網絡加載的 .wasm
文件,加載完成後才能進行任何的 source-map 解析。當使用 Node.js 把工具庫換成 fs.readFile
API 從硬盤中讀取 .wasm
文件。在這個腳本中,在進行任何 source-map 解析以前不須要執行初始化。咱們只負責提供一個統一的接口,基於什麼環境、用什麼的工具庫才能正確的加載 .wasm
文件,各位本身去擼代碼吧。
當編譯和實例化 WebAssembly 模塊時,咱們必須提供 mapping_callback
。這個回調函數不能在實例化 WebAssembly 模塊的生命週期外進行回調,可是能夠根據咱們將要執行的查找工做和不一樣的映射結果對返回結果進行一些調整。因此實際上 mapping_callback
只提供對分離後的映射成員進行對象結構化,而後把結果用一個閉包函數包裹起來後返回給你,你隨意進行查找操做。
let currentCallback = null;
// ...
WebAssembly.instantiate(buffer, {
env: {
mapping_callback: function (
generatedLine,
generatedColumn,
hasLastGeneratedColumn,
lastGeneratedColumn,
hasOriginal,
source,
originalLine,
originalColumn,
hasName,
name
) {
const mapping = new Mapping;
mapping.generatedLine = generatedLine;
mapping.generatedColumn = generatedColumn;
if (hasLastGeneratedColumn) {
mapping.lastGeneratedColumn = lastGeneratedColumn;
}
if (hasOriginal) {
mapping.source = source;
mapping.originalLine = originalLine;
mapping.originalColumn = originalColumn;
if (hasName) {
mapping.name = name;
}
}
currentCallback(mapping);
}
}
})
複製代碼
爲了 currentCallback
工程化和非工程化設置,咱們定義了 withMappingCallback
輔助函數來完成這件事:它就像設置過的 currentCallback
,若是不想設置的話直接調用 currentCallback
就能夠。一旦 withMappingCallback
完成,咱們就把 currentCallback
重置成 null
。RAII 等價於如下代碼:
function withMappingCallback(mappingCallback, f) {
currentCallback = mappingCallback;
try {
f();
} finally {
currentCallback = null;
}
}
複製代碼
回想如下 JavaScript 最初的設想,當解析一段 source-map 時,須要告訴 WebAssembly 分配一段內存來存儲 「映射集」
字符串,而後將字符串複製到一段 buffer 字節內存裏:
const size = mappingsString.length;
const mappingsBufPtr = this._wasm.exports.allocate_mappings(size);
const mappingsBuf = new Uint8Array(
this._wasm.exports.memory.buffer,
mappingsBufPtr,
size
);
for (let i = 0; i < size; i++) {
mappingsBuf[i] = mappingsString.charCodeAt(i);
}
複製代碼
JavaScript 對 buffer 字節進行初始化的時候,它會調用從 WebAssembly 導出的 parse_mappings
函數,若是轉換過程失敗就 拋出
一些 報錯
。
const mappingsPtr = this._wasm.exports.parse_mappings(mappingsBufPtr);
if (!mappingsPtr) {
const error = this._wasm.exports.get_last_error();
let msg = `Error parsing mappings (code ${error}): `;
// XXX: 用 `fitzgen/source-map-mappings` 同步接收報錯信息。
switch (error) {
case 1:
msg += "the mappings contained a negative line, column, source index or name index";
break;
case 2:
msg += "the mappings contained a number larger than 2**32";
break;
case 3:
msg += "reached EOF while in the middle of parsing a VLQ";
break;
case 4:
msg += "invalid base 64 character while parsing a VLQ";
break
default:
msg += "unknown error code";
break;
}
throw new Error(msg);
}
this._mappingsPtr = mappingsPtr;
複製代碼
運行在 WebAssembly 中的查找函數都有類似的結構,跟 Rust 語言定義的方法同樣。它們判斷傳入的查找參數,傳入一個臨時的閉包回調函數到 withMappingCallback
獲得返回值,將 withMappingCallback
傳入 WebAssembly 就獲得最終結果。
allGeneratedPositionsFor
在 JavaScript 中的實現以下:
BasicSourceMapConsumer.prototype.allGeneratedPositionsFor = function ({
source,
line,
column,
}) {
const hasColumn = column === undefined;
column = column || 0;
source = this._findSourceIndex(source);
if (source < 0) {
return [];
}
if (originalLine < 1) {
throw new Error("Line numbers must be >= 1");
}
if (originalColumn < 0) {
throw new Error("Column numbers must be >= 0");
}
const results = [];
this._wasm.withMappingCallback(
m => {
let lastColumn = m.lastGeneratedColumn;
if (this._computedColumnSpans && lastColumn === null) {
lastColumn = Infinity;
}
results.push({
line: m.generatedLine,
column: m.generatedColumn,
lastColumn,
});
}, () => {
this._wasm.exports.all_generated_locations_for(
this._getMappingsPtr(),
source,
line,
hasColumn,
column
);
}
);
return results;
};
複製代碼
當 JavaScript 查找 source-map,調用 SourceMapConsumer.prototype.destroy
方法,它會在內部調用從 WebAssembly 導出的 free_mappings
函數:
BasicSourceMapConsumer.prototype.destroy = function () {
if (this._mappingsPtr !== 0) {
this._wasm.exports.free_mappings(this._mappingsPtr);
this._mappingsPtr = 0;
}
};
複製代碼
全部測試都是運行在 2014 年年中生產的 MacBook Pro 上,具體配置是 2.8 GHz Intel i7 處理器,16 GB 1600 MHz DDR3 內存。筆記本電腦測試過程當中一直插入電源,而且在進行網頁基準測試時,每次測試開始前都刷新網頁。測試使用的瀏覽器的版本號非別是:Chrome Canary 65.0.3322.0, Firefox Nightly 59.0a1 (2018-01-15), Safari 11.0.2 (11604.4.7.1.6)[3]。爲了保證測試環境一致,在採集執行時間前都運行 5 次來 預熱
瀏覽器的 JIT 編譯器,而後計算運行 100 次的總時間。
咱們使用同一個 source-map 文件,選用文件中三個不一樣位置大小的片斷做爲測試素材:
用 JavaScript 實現的 壓縮版 source-map
。這個 source-map 文件用 UglifyJS 進行壓縮,最終的 「映射集」
字符串長度只有 30,081 個字符。
Angular.JS 最後版本壓縮獲得的 source-map,這個 「映射集」
字符串長度是 391,473 個字符。
Scala.JS 運行時的計算獲得 JavaScript 的 source-map
。這個映射體積最大,「映射集」
字符串長度是 14,964,446 個字符。
另外,咱們還專門增長兩種人爲的 source-map 結構:
將 Angular.JS source map 原體積擴大 10 倍。「映射集」
字符串長度是 3,914,739 個字符。
將 Scala.JS source map 原體積擴大 2 倍。「映射集」
字符串長度是 29,928,893 個字符。這個 source-map 在保持其餘基準的狀況下咱們只收集運行 40 次的時間。
精明的讀者可能會留意到,擴大後的 source-map 分別多出 9 個和 1 個字符,這多出的字符數量剛好是在擴大過程當中將 suorce-map 分隔開的 ;
。
咱們把目光集中到 Scala.JS source map,它是不通過人爲擴大時體積最大的版本。另外,它仍是咱們所測試的過的瀏覽器環境中體積最大的。用 Chrome 測試體積最大的 source-map 時什麼數據也沒有 (擴大 2 倍的 Scala.JS source map)。用 JavaScript 實現的版本,咱們無法經過組合模擬出 Chrome 標籤的內容進行崩潰;用 WebAssembly 實現的版本,Chrome 將會拋出 運行時錯誤:內存訪問超出界限
,使用 Chrome 的 debugger 工具,能夠發現是因爲 .wasm
文件缺乏內存泄漏時的處理指令。其餘瀏覽器在 WebAssembly 實現的版本都能成功經過基準測試,因此,我只能認爲這是 Chrome 瀏覽器的一個bug
對於基準測試,值越小測試效果越好
第一個基準測試程序經過在源碼打上斷點來進行分步調試。它須要 source-map 正在被解析成 「映射集」
字符串,並且解析獲得的映射以源碼出現的位置進行排列,這樣咱們就能夠經過二分查找的方法找到斷點對應 「映射集」
中的行號。查找結果返回編譯後的文件對應 JavaScript 源碼的定位。
WebAssembly 的實如今瀏覽器中的執行性能要全面優於 JavaScript 的實現。對於 Scala.JS source map,使用 WebAssembly 實現的版本運行時間在 Chrome 瀏覽器只有原來的 0.65x、在 Firefox 瀏覽器只有原來的 0.30x、在 Safari 瀏覽器只有原來的 0.37x。使用 WebAssembly 實現,運行時間最短的是 Safari 瀏覽器,平均只須要 702 ms,緊跟着的是 Firefox 瀏覽器須要 877 ms,最後是 Chrome 瀏覽器須要 1140 ms。
此外,相對偏差值,WebAssembly 實現要遠遠小於 JavaScript 實現的版本,尤爲是在 Firefox 瀏覽器中。以 Scala.JS source map 的 JavaScript 實現的版本爲例,Chrome 瀏覽器相對偏差值是 ±4.07%,Firefox 瀏覽器是 ±10.52%,Safari 瀏覽器是 ±6.02%。WebAssembly 實現的版本中,Chrome 瀏覽器的相對偏差值縮小到 ±1.74%,在 Firefox 瀏覽器 ±2.44%,在 Safari 瀏覽器 ±1.58%。
第二個基準測試用來補充第一個基準測試中的意外狀況。當逐步調試暫停並且捕獲到一個未知的異常,可是沒有生成 JavaScript 代碼,當一個控制檯打印信息沒有給出生成 JavaScript 代碼,或者逐步調試生成的 JavaScript 來自於其餘的 JavaScript 源碼,就啓用第二個基準測試方案。
對 JavaScript 源碼和編譯後的代碼進行定位時,「映射集」
字符串必須中止解析。已經解析好的映射通過排序建立 JavaScript 的定位,這樣就能夠經過二分查找定位到最接近的映射定位,根據映射定位找到最接近的源文件定位。
再一次的,在全部瀏覽器對 WebAssembly 和 JavaScript 這兩種實現多維評估模型測試,WebAssembly 在運行時間上遙遙領先。對比 Scala.JS source map,在 Chrome 瀏覽器中 WebAssembly 實現的版本只須要花費 JavaScript 的 0.23x。在 Firefox 瀏覽器和 Safari 瀏覽器中只須要花費 0.17x。Safari 瀏覽器運行 WebAssembly 最快 (305ms),緊接着是 Firefox 瀏覽器 (397ms),最後是 Chrome 瀏覽器 (486ms)。
WebAssembly 實現的結果偏差值也更小,對比 Scala.JS 的實現,在 Chrome瀏覽器中相對偏差值從 ±4.04% 降到 2.35±%,在 Firefox 瀏覽器從 ±13.75% 降到 ±2.03%,在 Safari 瀏覽器從 ±6.65% 降到 ±3.86%。
第三和第四個基準測試,經過觀察在第一個斷點緊接着又設置一個斷點,或者在發現異常暫停的位置後又設置暫停,或者轉換打印的運行日誌信息的時間花銷。按照以往,這些操做都不會成爲性能瓶頸:性能花銷最大的地方在於 「映射集」
字符串的解析和可查找數據的結構構建(對數組進行排序)。
話說是這麼說,咱們仍是但願能確保這些花銷能維持的更加 穩定
:咱們不但願這些操做會在某些條件下性能花銷忽然提升。
如下是在基準測試中,不一樣的編譯後文件定位到源文件的二分查找所花的時間。
這個基準測試比其餘基準測試的結果要更豐富。查看 Scala.JS source map 以不一樣的實現方式輸入到不一樣瀏覽器中能夠看到更細小的差別。由於都是用很小的時間單位去衡量測試結果,因此細小的時間差別也能顯現出來。咱們能夠看到 Chrome 瀏覽器只用了十分之一毫秒,Firefox 瀏覽器只用了 0.02 毫秒,Safari 瀏覽器用了 1 毫秒。
根據這些數據,咱們能夠得出結論,後續查詢操做在 JavaScript 和 WebAssembly 實現中大部分都保持在毫秒級如下。後續查詢歷來不會成爲用 WebAssembly 來從新實現時的瓶頸。
最後兩個基準測試的是解析 source-map 並當即遍歷全部映射所花的時間,並且遍歷的映射都是假定爲已經解析完畢的。這是一個很普通的操做,經過構建工具消耗和重建 source-map。它們有時也經過逐步調試器向用戶強調用戶能夠設置斷點的原始源內的哪些行 —— 在沒有轉換爲生成中的任何位置的 JavaScript 行上設置斷點沒有意義。
這些基準測試也有一個地方讓咱們十分擔心:它涉及了不少 JavaScript↔WebAssembly 兩種代碼相互穿插運行,在映射 source-map 時還要注意 FFI。對於全部基準測試,咱們已經最大限度的減小這種 FFI 調用。
事實證實,咱們的擔憂是多餘的。 WebAssembly 實現不只知足 JavaScript 實現的性能,即便 source-map 已被解析,也超過了 JavaScript 實現的性能。對於分析迭代和迭代已解析的基準測試,WebAssembly 在 Chrome 瀏覽器中的時間花費是 JavaScript 的 0.61 倍和 0.71 倍。在 Firefox 瀏覽器中,WebAssembly 的時間花費 JavaScript 的 0.56 倍和 0.77 倍。在 Safari 瀏覽器中,WebAssembly 實現是 JavaScript 實現的時間 0.63 倍和 0.87倍。 Safari 瀏覽器再一次以最快的速度運行 WebAssembly 實現,Firefox 瀏覽器和 Chrome 瀏覽器基本上排在第二位。 Safari 瀏覽器在迭代已解析的基準測試中值得對 JavaScript 性能給予特別優化:除了超越其餘瀏覽器的 JavaScript 時間以外,Safari 瀏覽器運行 JavaScript 的速度比其餘瀏覽器運行WebAssembly 的速度還要快!
這符合早期基準測試趨勢,咱們還看到 WebAssembly 相對偏差比 JavaScript 的相對偏差要小。通過解析和遍歷,Chrome 瀏覽器的相對偏差從 ±1.80% 降到 ±0.33%,Firefox 瀏覽器從 ±11.63% 降到 ±1.41%,Safari 瀏覽器從 ±2.73% 降到 ±1.51%。當遍歷一個已經解析完的映射,Firefox 瀏覽器的相對偏差從 ±12.56% 降到 ±1.40%,Safari 瀏覽器從 ±1.97% 降到 ±1.40%。Chrome 瀏覽器的相對偏差從 ±0.61% 升到 ±1.18%,這是基準測試中惟一一個趨勢上升的瀏覽器。
使用 wasm32-unknown-unknown
比 wasm32-unknown-emscripten
的好處在於生成的 WebAssembly 代碼體積更小。wasm32-unknown-emscripten
包含了許多補丁,好比 libc
,好比在文件系統頂部創建 IndexedDB
,對於 source-map
庫,咱們只使用 wasm32-unknown-unknown
。
咱們考慮的是最終交付到客戶端的 JavaScript 和 WebAssembly 代碼體積。也就是說,咱們在將 JavaScript 模塊捆綁到一個 .js
文件後查看代碼大小。咱們看看使用 wasm-gc
,wasm-snip
和 wasm-opt
縮小 .wasm
文件體積的效果,以及使用網頁上都支持的 gzip
壓縮。
在這個衡量標準下,JavaScript 的體積老是指壓縮後的大小, 用 Google Closure 編譯器 建立屬於 「簡單」 的優化級別。咱們使用 Closure Compiler 只由於 UglifyJS 對於一些新的 ECMAScript 標準無效(例如 let
和箭頭函數)。咱們使用 「簡單」 的優化級別,由於 「高級」 優化級別對於沒有用 Closure Compiler 編寫的 JavaScript 具備破壞性。
標記爲 「JavaScript」 的條形圖用於原始的純 JavaScript source-map
庫實現的變體。標記爲 「WebAssembly」 的條形圖用於新的 source-map
庫實現的變體,它使用 WebAssembly 來解析字符串的 「映射」 並查詢解析的映射。請注意,「WebAssembly」 實現仍然使用 JavaScript 來實現全部其餘功能! source-map
庫有額外的功能,好比生成映射地圖,這些功能仍然在 JavaScript 中實現。對於 「WebAssembly」 實現,咱們報告 WebAssembly 和 JavaScript 的大小。
在最小處,新的 WebAssembly 實現總代碼體積要比舊的 JavaScript 實現大不少:分別是 20,996 字節與 8,365字節。儘管如此,使用 .wasm
的工具進行代碼壓縮,獲得的 WebAssembly 文件只有原來體積的 0.16 倍。代碼量跟 JavaScript 差很少。
若是咱們用 WebAssembly 替換 JavaScript 解析和查詢代碼,爲何 WebAssembly 實現不包含更少的 JavaScript?有兩個因素致使 JavaScript 沒法剔除。首先,須要引入一些新的 JavaScript 來加載 .wasm
文件並給 WebAssembly 提供接口。其次,更重要的是,咱們 「替換」 的一些 JavaScript 事務與 suorce-map
庫的其餘部分共享。雖然如今事務已經再也不共享,可是其餘庫可能仍然在使用。
讓咱們把目光投向 gzip
壓縮過的 .wasm
文件。運行 wasm-objdump -h
給出每一部分的體積:
Code
和 Data
幾乎佔據了 .wasm
文件的體積。Code
部分包含組成函數體的 WebAssembly 編碼指令。Data
部分包含要加載到 WebAssembly 模塊的連續內存空間中的靜態數據。
使用 wasm-objdump
手動檢查 Data
部分的內容,顯示它主要由用於構建診斷消息的字符串片斷組成,好比 Rust 代碼運行出錯的。可是,在定位 WebAssembly 時,Rust 運行錯誤會轉化爲 WebAssembly 陷阱,而且陷阱不會攜帶額外的診斷信息。咱們認爲這是 rustc
中的一個錯誤,即這些字符串片斷被提交出去。不幸的是,wasm-gc
目前還不能移除沒有使用過的 Data
片斷,因此咱們在這段時間內一直處於這種臃腫的狀態。WebAssembly 和相關工具仍然不成熟,咱們但願工具鏈隨着時間的推移在這方面獲得改進。
接下來,咱們對 wasm-objdump
的反彙編輸出進行後處理,以計算 Code
部分中每一個函數體的大小,並獲得用 Rust 建立時的大小:
最重要的代碼塊是 dlmalloc
,它經過 alloc
實現 Rust 底層的內存分配 APIs。dlmalloc
和 alloc
加起來一共是 10,126 字節,佔總函數代碼量的 50.98%。從某種意義上說,這是一種解脫:分配器的代碼大小是一個常數,不會隨着咱們將更多的 JavaScript 代碼移植到 Rust 而增加。
咱們本身實現的代碼總量是(vlq
,source_map_mappings
和 source_map_mappings_wasm_api
)9,320 字節,佔總函數體積的 46.92%。只留了 417 字節(2.10%)給其它函數。這足以說明 wasm-gc
,wasm-snip
和 wasm-opt
的功效:std
比咱們的代碼要多,但咱們只使用了一小部分 API,因此只保留咱們用過的函數。
用 Rust 和 WebAssembly 重構 source-map 中性能最敏感的解析和查找的功能已經完成。在咱們的基準測試中,WebAssembly 實現只須要原始 JavaScript 實現所花費時間的一小部分 —— 僅爲 0.17倍。咱們觀察到在全部瀏覽器中,WebAssembly 實現老是比 JavaScript 實現的性能要好。WebAssembly 實現也比 JavaScript 實現更加一致和可靠的性能:WebAssembly 實現的進行遍歷操做的時間相對偏差值更小。
JavaScript 已經以性能的名義積累了許多使人費解的代碼,咱們用可讀性更好的 Rust 替代了它。Rust 並不強迫咱們在清晰表達意圖和運行時間表現之間進行選擇。
換句話說,咱們仍然要爲此作許多工做。
下一步工做的首要目標是完全瞭解爲何 Rust 標準庫的排序在 WebAssembly 中沒有達到咱們實現的快排性能。這個表現另咱們驚訝不已,由於咱們實現的快排依舊很粗糙,而標準庫的快排在模式設計上很失敗,投機性的使用了最小插入排序和大範圍排序。事實上,在原生環境下,標準庫的排序性能要比咱們實現的排序要好。咱們推測是內聯函數引發運行目標轉移,而咱們的比較函數沒有內聯到標準庫中,因此當目標轉移到 WebAssembly 時,標準庫的排序性能就會降低。這須要進一步的驗證。
咱們發現 WebAssembly 體積分析太困難而顯得不是很必要。爲了得到更有意義的信息,咱們只能編寫 咱們本身實現的反編譯腳本 wasm-objdump
。該腳本構造調用圖,並讓咱們查詢某些函數的調用者是誰,幫助咱們理解爲何該函數是在 .wasm
文件中被提交,即便咱們沒有預料到它。很很差意思,這個腳本對內聯函數不起做用。一個適當的 WebAssembly 體積分析器會有所幫助,而且任何人都能從追蹤獲得有用的信息。
內存分配器的代碼體積相對較大,重構或者調整一個分配器的代碼量能夠爲 WebAssembly 生態系統提供至關大的做用。至少對於咱們的用例,內存分配器的性能幾乎不用考慮,咱們只須要手動分配很小的動態內存。對於內存分配器,咱們會絕不猶豫的選擇代碼體積小的。
Data
部分中沒有使用的片斷須要用 wasm-gc
或者其餘工具進行高亮,檢測和刪除永遠不會被使用的靜態數據。
咱們仍然能夠對庫的下游用戶進行一些 JavaScript API 改進。在咱們當前的實現中引入 WebAssembly 須要引入在用戶完成映射解析時手動釋放內存。對於大多數習慣依賴垃圾回收器的 JavaScript 程序員來講,這並不是天然而然,他們一般不會考慮任何特定對象的生命週期。咱們能夠傳入 SourceMapConsumer.with
函數,它包含一個未解析的 source-map 和一個 async
函數。 with
函數將構造一個 SourceMapConsumer
實例,用它調用 async
函數,而後在 async
函數調用完成後調用 SourceMapConsumer
實例的 destroy
。這就像 JavaScript 的async
RAII。
SourceMapConsumer.with = async function (rawSourceMap, f) {
const consumer = await new SourceMapConsumer(rawSourceMap);
try {
await f(consumer);
} finally {
consumer.destroy();
}
};
複製代碼
另外一個使 API 更容易被 JavaScript 編程人員使用的方法是把 SourceMapConsumer
傳入每個 WebAssembly 模塊。由於 SourceMapConsumer
實例佔據了 WebAssembly 模塊實例的 GC 邊緣,垃圾回收器就管理了 SourceMapConsumer
實例、WebAssembly 模塊實例和模塊實例堆。經過這個策略,咱們用一個簡單的 static mut MAPPINGS: Mappings
就能夠把 Rust 和 WebAssembly 膠粘起來,而且 Mapping
實例在全部導出的查找函數都是不可見的。在 parse_mappings
函數中再也不有 Box :: new(mappings)
,而且再也不傳遞 * mut Mappings
指針。謹慎期間,咱們可能須要把 Rust 庫全部內存分配函數移除,這樣能夠把須要提交的 WebAssembly 體積縮小一半。固然,這一切都取決於建立相同 WebAssembly 模塊的多個實例是一個相對簡單的操做,這須要進一步調查。
wasm-bindgen
項目的目標是移除全部須要手動編寫的 FFI 膠粘代碼,實現 WebAssembly 和 JavaScript 的自動化對接。使用它,咱們可以刪除全部涉及將 Rust API 導出到 JavaScript 的手寫 不安全
指針操做代碼。
在這個項目中,咱們將 source-map 解析和查詢移植到 Rust 和 WebAssembly 中,但這只是 source-map
庫功能的一半。另外一半是生成源映射,它也是性能敏感的。咱們但願在將來的某個時候重寫 Rust 和 WebAssembly 中構建和編碼源映射的核心。咱們但願未來能看到生成源映射也能達到這樣的性能。
WebAssembly 實現的 mozilla/source-map
庫全部提交申請的合集 這個提交申請包含了基準測試代碼,能夠將結果重現,你也能夠繼續完善它。
最後,我想感謝 Tom Tromey 對這個項目的支持。同時也感謝 Aaron Turon、Alex Crichton、Benjamin Bouvier、Jeena Lee、Jim Blandy、Lin Clark、Luke Wagner、Mike Cooper 以及 Till Schneidereit 閱審閱原稿並提供了寶貴的意見。很是感謝他們對基準測試代碼和 source-map
庫的貢獻。
[1] 當你傳入本身定義的對比函數,SpiderMonkey 引擎會使用 JavaScript 數組原型的排序方法 Array.prototype.sort
;若是不傳入對比函數,SpiderMonkey 引擎會使用 C++ 實現的排序方法
[2] 一旦 Firefox 瀏覽器出現 1319203 錯誤碼,WebAssembly 和 JavaScript 之間的調用性能將會急速降低。WebAssembly 和 JavaScript 的調用和 JavaScript 之間的調用開銷都是非線性增加的,截止本文發表前各大瀏覽器廠商仍然沒能改進這個問題。
[3] Firefox 瀏覽器和 Chrome 瀏覽器咱們都進行了 每日構建
測試,可是沒有對 Safari 瀏覽器進行這樣的測試。由於最新的 Safari Technology Preview 須要比 El Capitan 更新的 macOS 版本,而這款電腦就運行這個版本了。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。