[译] 论 Rust 和 WebAssembly 对源码地址索引的极限优化

Tom Tromey 和我尝试使用 Rust 语言进行编码,而后用 WebAssembly 进行编译打包后替换 source-map(源码地址索引,如下行文为了理解方便均不进行翻译)的 JavaScript 工具库中性能敏感的部分。在实际场景中以相同的基准进行对比操做,WebAssembly 的性能要比已有的 source-map 库快上 5.89 倍。另外,屡次测试结果也更为一致:相对一致的状况下误差值很小。前端

咱们以提升性能的名义将那些使人费解又难以阅读的 JavaScript 代码替换成更加语义化的 Rust 代码,这确实行之有效。android

如今,咱们把 Rust 结合 WebAssembly 使用的经验分享给你们,也鼓励程序员按照本身的需求对性能敏感的 JavaScript 进行重构。ios

背景

source-map 的技术规范

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

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 有诸如 rustupcargocrates.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>
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,咱们定义的排序函数比标准库的内置排序函数执行效率更高。(对于这样的差别很意外,不过咱们也没有再深究了。)

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
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);
    }
}
复制代码

将 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 重置成 nullRAII 等价于如下代码:

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 文件,选用文件中三个不一样位置大小的片断做为测试素材:

  1. 用 JavaScript 实现的 压缩版 source-map。这个 source-map 文件用 UglifyJS 进行压缩,最终的 “映射集” 字符串长度只有 30,081 个字符。

  2. Angular.JS 最后版本压缩获得的 source-map,这个 “映射集” 字符串长度是 391,473 个字符。

  3. Scala.JS 运行时的计算获得 JavaScriptsource-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 TuronAlex CrichtonBenjamin BouvierJeena LeeJim BlandyLin ClarkLuke WagnerMike 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 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索