[譯] 論 Rust 和 WebAssembly 對源碼地址索引的極限優化

  • 原文地址: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 代碼,這確實行之有效。

現在,我們把 Rust 結合 WebAssembly 使用的經驗分享給大家,也鼓勵程序員按照自己的需求對性能敏感的 JavaScript 進行重構。

背景

source-map 的技術規范

source map 文件提供了 JavaScript 源碼被編譯器[0]、壓縮工具、包管理工具轉譯成的文件之間的地址索引供編程人員使用。JavaScript 開發者工具使用 source-map 后可以實現字符級別的回溯,調試工具中的按步調試也是依賴它來實現的。Source-map 對報錯信息的編碼方式與 DWARF’s .debug_line 的部分標準很相似。

source-map 對象是 JSON 對象的其中一個分支。其中 “映射集” 用字符串表示,是 source-map 的重要組成部分,包含了最終代碼和定位對象的雙向索引。

我們用 extended Backus-Naur form (EBNF) 標準描述 “映射集” 的字符串語法。

Mappings 是 JavaScript 代碼塊的分組行號,每一個映射集只要以分號結尾了就代表一個獨立的映射集,它就自增 1。同一行 JavaScript 代碼如果生成多個映射集,就用逗號分隔開:

<mappings> = [ <generated-line> ] ';' <mappings>| '';<generated-line> = <mapping>| <mapping> ',' <generated-line>;
復制代碼

每一個獨立的映射集都能定位到當初生成它的那段 JavaScript 代碼,還能有一個關聯名字的可選項能定位到那段代碼中的源碼字符串:

<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 編碼后的二進制數前五位用來表示數值,最后一位只用來做標記正負。

與其向你解釋 EBNF 標準,不如來看一段簡單的 VLQ 轉換代碼實現:

constant SHIFT = 5
constant CONTINUATION_BIT = 1 << SHIFT
constant MASK = (1 << SHIFT) - 1decode_vlq(input):let accumulation = 0let shift = 0let next_digit_part_of_this_number = true;while next_digit_part_of_this_number:let [character, ...rest] = inputlet digit = decode_base_64(character)accumulation += (digit & MASK) << shiftshift += SHIFT;next_digit_part_of_this_number = (digit & CONTINUATION_BIT) != 0input = restlet is_negative = accumulation & 1let value = accumulation >> 1if 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

WebAssembly 是一種新的技術,它以二進制形式運行在 Web 瀏覽器底層,為瀏覽器隔離危險代碼和減少代碼量所設計的。現在已經作為 Web 的標準,而且大多數的瀏覽器廠商已經支持這個功能。

WebAssembly 開辟一塊新的棧區供機器運行,有現代處理器架構的支持能更好的處理映射,它可以直接操作一大塊連續的儲存 buffer 字節。WebAssembly 不支持自動化的垃圾回收,不過 在不久的將來 它也會繼承 JavaScript 對象的垃圾回收機制。控制流是具有結構化的,比起在代碼間隨意的打標記或者跳躍,它被設計用來提供一種更可靠、運行一致的執行流程。處理一些架構上的邊緣問題,比如:超出表示范圍的數值怎么截取、溢出問題、規范 NaN

WebAssembly 的目標是獲得或者逼近原始指令的運行速度。目前在大多數的基準測試中跟原始指令相比 只相差 1.5x 了。

因為缺乏垃圾收集器,要編譯成 WebAssembly 語言僅限那些沒有運行時和垃圾采集器的編程語言,除非把控制器和運行時也編譯成 WebAssembly。實際中這些一般很難做到。現在,語言開發者事實上是把 C,C++ 和 Rust 編譯成 WebAssembly。

Rust

Rust 是一種更加安全和高效的系統編程語言。它的內存管理更加安全,不依賴于垃圾回收機制,而是允許你通過靜態追蹤函數 ownershipborrowing 這兩個方法來申請和釋放內存。

使用 Rust 來編譯成 WebAssembly 是一種不錯的選擇。由于語言設計者一開始就沒有為 Rust 設計垃圾自動回收機制,也就不用為了編譯成 WebAssembly 做額外的工作。Web 開發者還發現一些在 C 和 C++ 沒有的優點:

  • Rust 庫更加容易構建、容易共享、打包簡單和容易提取公共部分,而且自成文檔。Rust 有諸如 rustupcargo 和 crates.io 的完整生態系統。這是 C 和 C++ 所不能比擬的。
  • 內存安全方面。在 迭代算法 中不斷產生內存碎片。Rust 則可以在編譯時就避免大部分類似的性能陷阱。

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 函數對 “映射集” 進行解析和查找。

Base 64 大數值的位數可變表示法

對 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>
whereB: 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>
whereB: 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,我們定義的排序函數比標準庫的內置排序函數執行效率更高。(對于這樣的差異很意外,不過我們也沒有再深究了。)

JavaScript 接口

WebAssembly 的對外函數接口(foreign function interface,簡稱 FFI)受限于標量值,所以一些以 Rust 語言編寫,通過 WebAssembly 轉成 JavaScript 代碼后的函數參數只能是標量數值類型,返回值也是標量數值類型。因此,JavaScript 要求 Rust 為 “映射集” 字符串分配一塊緩沖區并返回該 buffer 字節的地址指針。然后,JavaScript 必須復制出 “映射集” 字符串的 buffer 字節,這時候因為 FFI 的限制什么也做不了,只能把整段連續的 WebAssembly 內存直接寫入。之后 JavaScript 調用 parse_mappings 函數進行 buffer 字節的初始化工作,初始化完畢后返回解析結果的指針。完成上述這些前置工作后,JavaScript 就可以使用 WebAssembly 的 API ,給定一些數值查找結果,或者給定一個指針得到解析后的映射集。所有查詢結果完畢以后,JavaScript 會告訴 WebAssembly 釋放存儲映射集結果的內存空間。

從 Rust 暴露 WebAssembly 的應用編程接口

所有的暴露出去的 WebAssembly APIs 都被封裝在一個 “小膠箱” 里。這樣的分離很有用,它允許我們用測試環境來執行 source-map-mappings。如果你想編譯成純的 WebAssembly 代碼也可以,只需要把編譯環境修改成 WebAssembly。

另外,受限于 FFI 的傳值要求,那么輸出的函數必須滿足一下兩點:

  • 它不能有 #[無名] 屬性,要方便 JavaScript 能調用它 。
  • 它標記 外部 "C" 以便提取到 .wasm 公共文件中。

不同于核心庫,這些代碼暴露功能給 WebAssembly 轉 JavaScript,有必要提醒你,頻繁使用非常的 不安全。 只要調用 外部 函數和使用指針從 FFI 邊界接收指針,就是 不安全,因為 Rust 編譯器沒法校驗另一端是否安全。我們很少關心到這個安全性問題 —— 最壞的情況下我們可以做一個 陷阱(把 JavaScript 端的 報錯 全部抓住),或者直接返回一個報錯響應。在同一段地址中,可以向地址寫入內容要比只是將地址儲存的內容以二進制字節運行要危險的多,如果可寫入的話,攻擊者就可以欺騙程序跳轉到特定的內存地址,然后插入一段他自己的 shell 腳本代碼。

我們輸出的一個最簡單是函數功能是把工具庫產生的一個報錯捕獲到。它提供了 libcerrno 類似的功能,它會將 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>> 添加一個連接層或者保存在外部數據中,另一端有需要這份數據的時候再載體進行格式化。我們決定采用后一個方法。

這個載體由以下三者組成:

  1. 一個指針指向堆內存元素,
  2. 分配內存的容量有多大,
  3. 元素的初始化長度。

當我們暴露一個堆內存元素的指針給 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
whereT: ?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);}
}
復制代碼

將 Rust 編譯成 .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-gcwasm-snipwasm-gcwasm-opt

在 JavaScript 使用 WebAssembly APIs

在 JavaScript 使用 WebAssembly 的首要問題就是,如何加載 .wasm 文件。 source-map 工具庫的運行環境主要有三個:

  1. Node.js
  2. 網頁
  3. 火狐開發者工具里

不同的環境使用不同的方式將 .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";breakdefault: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 文件,選用文件中三個不同位置大小的片段作為測試素材:

  1. 用 JavaScript 實現的 壓縮版 source-map。這個 source-map 文件用 UglifyJS 進行壓縮,最終的 “映射集” 字符串長度只有 30,081 個字符。

  2. Angular.JS 最后版本壓縮得到的 source-map,這個 “映射集” 字符串長度是 391,473 個字符。

  3. Scala.JS 運行時的計算得到 JavaScript 的 source-map。這個映射體積最大,“映射集” 字符串長度是 14,964,446 個字符。

另外,我們還專門增加兩種人為的 source-map 結構:

  1. 將 Angular.JS source map 原體積擴大 10 倍。“映射集” 字符串長度是 3,914,739 個字符。

  2. 將 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-unknownwasm32-unknown-emscripten 的好處在于生成的 WebAssembly 代碼體積更小。wasm32-unknown-emscripten 包含了許多補丁,比如 libc,比如在文件系統頂部建立 IndexedDB,對于 source-map 庫,我們只使用 wasm32-unknown-unknown

我們考慮的是最終交付到客戶端的 JavaScript 和 WebAssembly 代碼體積。也就是說,我們在將 JavaScript 模塊捆綁到一個 .js 文件后查看代碼大小。我們看看使用 wasm-gcwasm-snipwasm-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 給出每一部分的體積:

CodeData 幾乎占據了 .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。dlmallocalloc 加起來一共是 10,126 字節,占總函數代碼量的 50.98%。從某種意義上說,這是一種解脫:分配器的代碼大小是一個常數,不會隨著我們將更多的 JavaScript 代碼移植到 Rust 而增長。

我們自己實現的代碼總量是(vlqsource_map_mappingssource_map_mappings_wasm_api)9,320 字節,占總函數體積的 46.92%。只留了 417 字節(2.10%)給其它函數。這足以說明 wasm-gcwasm-snipwasm-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 庫的貢獻。


[0] 或者你堅持叫做 “轉譯器”

[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、前端、后端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/451177.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/451177.shtml
英文地址,請注明出處:http://en.pswp.cn/news/451177.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Java WebService 簡單實例

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 前言&#xff1a;朋友們開始以下教程前&#xff0c;請先看第五大點的注意事項&#xff0c;以避免不必要的重復操作。 一、準備工作&…

互聯網侵入手機逐鹿背后:追求流量變現能力

摘要&#xff1a;小米聯合創始人黎萬強說&#xff0c;賣出10萬臺得免速死&#xff0c;賣出百萬臺算是得到了一張正式入行的門票。小米是一家新創公司&#xff0c;黎萬強自己說&#xff0c;原本一無所有&#xff0c;作為原創品牌&#xff0c;它選擇了口碑之路&#xff0c;則必須…

java api使用ElastichSearch指南

AggregationBuilders.terms:一段時間內&#xff0c;某個字段取值的數量排名前幾的聚合 / ** param startTime 開始的時間* param endTime 結束的時間* param termAggName term過濾* param fieldName 要做count的字段* param top 返回的數量*/ RangeQueryBuilder actionPeriod …

關于JavaScript的數組隨機排序

昨天了解了一下Fisher–Yates shuffle費雪耶茲隨機置亂算法&#xff0c;現在再來看看下面這個曾經網上常見的一個寫法&#xff1a; function shuffle(arr) { arr.sort(function () { return Math.random() - 0.5; }); } 或者使用更簡潔的 ES6 的寫法&#xff1a; function shu…

通用唯一識別碼UUID

UUID是通用唯一識別碼&#xff08;Universally Unique Identifier&#xff09;的縮寫。UUID 的目的&#xff0c;是讓分布式系統中的所有元素&#xff0c;都能有唯一的辨識資訊&#xff0c;而不需要透過中央控制端來做辨識資訊的指定。如此一來&#xff0c;每個人都可以建立不與…

java內省機制 + 內省是什么 + 內省實現方式 + 和反射的區別

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一、內省是什么、實現方式&#xff1a; 內省&#xff08;Introspector&#xff09;是Java語言對Bean類屬性、事件的一種缺省處理方法。…

百度聯合長虹發布第二款云手機 售價900元以下

摘要&#xff1a;【搜狐IT消息】5月15日消息&#xff0c;百度今天宣布聯合長虹發布第二款智能手機&#xff0c;采用3.5英寸屏幕、300萬像素攝像頭&#xff0c;650MHz主頻處理器&#xff0c;零售價格在700-899元之間&#xff0c;中國聯通將為其提供話費補貼。 【搜狐IT消息】5月…

vmware workstation17環境安裝centos7

打開控制面板&#xff0c;搜索“服務”&#xff0c;啟動vmware authorize service -------解決無法開啟虛擬機問題之無法連接MKS 2.虛擬機硬盤擴展為15G------解決安裝centos7時出現的“檢查存儲配置出錯”問題 3.硬盤分區----/boot 300mb&#xff08;不能小于200mb&#xff0…

博客園中的源代碼格式顯示

昨天寫了一篇文章&#xff0c;但是在寫的時候呢&#xff0c;沒有注意&#xff0c;直接將代碼復制上去了&#xff0c;今天正好有人提醒&#xff0c;看到了格式的混亂&#xff0c;借此記錄整理一下&#xff0c;如何能直接粘貼代碼&#xff0c;而且格式&#xff08;縮進&#xff0…

static的使用

類中的靜態變量在程序運行期間&#xff0c;其內存空間對所有該類的對象實例而言是共享的&#xff0c;為了節省系統內存開銷、共享資源&#xff0c;應該對一些適合使用static的變量聲明為靜態變量。 變量聲明為static的使用場景&#xff1a; &#xff08;1&#xff09;變量所…

Linux內核的裁剪和移植

linux內核的裁剪和移植具體都在這個網址里面。https://blog.csdn.net/xie0812/article/details/10816059https://blog.csdn.net/xie0812/article/details/10821779轉載于:https://blog.51cto.com/13401435/2145947

李開復唱衰互聯網手機:大部分公司會失敗

摘要&#xff1a;互聯網企業和手機制造企業之間巨大的鴻溝也被李開復鮮明地指出來&#xff1a;“兩個產業差別巨大&#xff0c;企業基因不同。”百度此前也坦誠表示&#xff0c;與長虹合作的千元機&#xff0c;主要是針對2000元以下的用戶體驗&#xff0c;不能與四五千元的蘋果…

【POJ】3268 Silver Cow Party

題目鏈接&#xff1a;http://poj.org/problem?id3268 題意 &#xff1a;有N頭奶牛&#xff0c;M條單向路。X奶牛開party&#xff0c;其他奶牛要去它那里。每頭奶牛去完X那里還要返回。去回都是走的最短路。現在問這里面哪頭奶牛走的路最長。 題解&#xff1a;對每個奶牛i與X做…

java.util.ConcurrentModificationException異常分析

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 Java在操作ArrayList、HashMap、TreeMap等容器類時&#xff0c;遇到了java.util.ConcurrentModificationException異常。以ArrayList為例…

redis基本數據類型之String

redis基本數據類型之String redis一共分為5中基本數據類型&#xff1a;String,Hash,List,Set,ZSet String String類型是包含很多種類型的特殊類型&#xff0c;并且是二進制安全的。比如序列化的對象進行儲存&#xff0c;比如一張圖片進行二進制儲存&#xff0c;比如一個簡單…

Laravel5.5之事件監聽、任務調度、隊列

一、事件監聽 流程&#xff1a; 1.1 創建event php artisan make:event UserLogin LoginController.php /*** The user has been authenticated.** param \Illuminate\Http\Request $request* param mixed $user* return mixed*/protected function authenticated(Request …

朱江洪功成身退 朱董配解體誰主格力(圖)

摘要&#xff1a;中國家電營銷委員會副理事長洪仕斌向時代周報記者表示&#xff1a;“朱江洪和董明珠已經完成了他們在格力發展前二十年的使命。“朱董配”解體之后&#xff0c;有人質疑格力“技術營銷”的格局必將被打破&#xff0c;難以延續&#xff0c;“董氏班底”與朱江洪…

一些dos下簡單命令

(1)切換盤符 d: 回車 (2)顯示某目錄下的所有文件或者文件夾(掌握) dir 回車 (3)創建文件夾 md 文件夾名稱 回車 (4)刪除文件夾 rd 文件夾名稱 回車 (5)進入目錄(掌握) 單級進入 cd 目錄名稱 多級進入 cd 目錄名稱1\目錄名稱2\... (6)回退目錄(掌握) 單級回退 cd.. …

ssh服務器拒絕了密碼 請再試一次 Xftp5連接失敗

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 我的情況都很簡單&#xff1a; 第一回主機 ip 不對&#xff0c; 第二次 是賬號、密碼都不對。 最后 IP、賬號、密碼都對了 就連上了。

后端DTO(數據傳輸對象)與DAO(數據庫數據源對象)解耦的好處

我們在后端的開發中經常會將DO對象傳到Service層直接作為DTO傳給前端&#xff0c;這樣做其實會有很多弊端。 &#xff08;一&#xff09;DO對象一般其成員域和數據庫字段是對應的&#xff0c;所以不能添加額外的字段&#xff0c;但是有時候端就是需要這個字段。反之前端要向后…