跳到主要内容

2 篇博文 含有标签「JavaScript」

查看所有标签

使用 Chrome 对 MoonBit 生成的 Wasm 进行性能分析

· 阅读需 10 分钟

cover

我们前一篇博客中,我们介绍了如何在前端 JavaScript 中使用 MoonBit 驱动的 Wasm 库 Cmark。在本文中,我们将探索如何直接从 Chrome 浏览器中对该库进行性能分析。希望这篇教程能对你在使用 MoonBit 在类似的场景中进行开发时提供一些洞察,从而实现更好的整体性能。

具体而言,在本文中,我们将重点介绍如何对我们之前在前端应用中使用 Cmark 的示例做最小程度的修改,以便将 Chrome 内置的 V8 性能分析器应用到 Cmark 的 Wasm 代码中。

对 Cmark 库进行性能分析

我们很容易可以重构原始的前端应用程序,并添加一个新的导航栏以包含 “Demo” 和 “Profiling” 两个链接。点击第一个链接将使浏览器渲染原本的 A Tour of MoonBit for Beginners 示例的 HTML,点击第二个链接将导航到我们的新文档进行性能分析。(如果你对实际实现感兴趣,你可以在本文的末尾找到最终代码的链接。)

现在,我们已经准备为实际实现 Wasm 性能分析着手编写一些代码了。那么,与 JavaScript 性能分析相比,进行 Wasm 性能分析是否有不同之处?

实际上,我们可以使用与 JavaScript 相同的 API 来进行 Wasm 性能分析。Chromium 文档中有一篇文章详细描述了这些 API,简而言之:

  • 当我们调用 console.profile() 时,V8 性能分析器将开始记录 CPU 性能概况;
  • 之后,我们可以调用我们希望分析的性能关键函数;
  • 最后,当我们调用 console.profileEnd() 时,性能分析器将停止记录,并将结果数据可视化显示在 Chrome 的性能标签页中。

考虑到这一点,让我们来看一下实际的性能分析功能实现:

async function profileView() {
const docText = await fetchDocText("../public/spec.md");
console.profile();
const res = cmarkWASM(docText);
console.profileEnd();
return (
`<h2>Note</h2>
<p>
<strong
>Open your browser's
<a
href="https://developer.chrome.com/docs/devtools/performance"
rel="nofollow"
>Performance Tab</a
>
and refresh this page to see the CPU profile.</strong
>
</p>` + res
);
}

如你所见,我们必须最小化性能分析器激活期间执行的代码范围。因此,我们在该范围内仅调用了 cmarkWASM() 函数。

另一方面,我们选择了 《CommonMark 规范》 的 0.31.2 版本(即前文中提到的 spec.md)作为性能分析模式的输入文档。我们选择这一文档的主要原因是该文档使用了许多不同的 Markdown 特性,同时它的长度也足以使许多 Markdown 解析器遇到问题:

> wc -l spec.md  # 行数
9756 spec.md
> wc -w spec.md # 词数
25412 spec.md

我们重新组织了前端应用程序,使得点击导航栏中的 “Profiling” 链接将触发上面定义的 profileView() 函数,从而得到以下结果:

profile-tab-stripped

如果你曾深入研究过性能优化,那么你应该不会对这个火焰图应该感到陌生了...

等等,wasm-function[679]wasm-function[367] 这些名字又是什么?我们该如何知道哪个函数对应哪个编号?

事实证明,我们需要在构建 Wasm 文件时保留一些调试信息。毕竟,我们一直使用以下命令构建我们的 MoonBit 项目:

> moon -C cmarkwrap build --release --target=wasm-gc

...而去除调试信息是 moon 在生成 release 版本时的标准行为。

幸运的是,我们可以使用一个额外的标志 --no-strip 来保留符号信息,而不必依赖于慢速的 debug 版本。让我们使用这个标志重新构建项目:

> moon -C cmarkwrap build --release --no-strip --target=wasm-gc

注意

类似地,如果我们想对生成的 Wasm 文件使用 wasm-opt,可以使用 wasm-opt--debuginfo(或 -g)标志来保留优化后输出中的函数名。

保留函数名后,我们终于可以在性能标签页中看到实际情况了!

profile-tab

分析火焰图

如上图所示的火焰图可以很好地总结函数调用及其各自的执行时间。如果你不太熟悉它,火焰图的主要思路如下:

  • Y 轴 表示调用栈,最顶部的函数最先被调用;
  • X 轴 表示执行时间,每个矩形节点的宽度对应于某个函数及其子函数的调用所花费的总时间。

既然我们正在研究 Cmark 库的性能,我们应该向下检索,并特别注意那里的 @rami3l/cmark/cmark_html.render() 节点。例如,在该节点处,我们可以清楚地看到 render() 的执行被分成了两个主要部分,分别被图中的两个子节点代表:

  • @rami3l/cmark/cmark.Doc::from_string(),表示将输入的 Markdown 文档转换为语法树;
  • @rami3l/cmark/cmark_html.from_doc(),表示将语法树渲染为最终的 HTML 文档。

为了更好地查看性能情况,我们可以单击火焰图中的 render() 节点。这会让 Chrome 更新“自底向上”视图,并仅显示由 render() 递归调用的函数。这样,我们将会得到如下所示的结果:

profile-tab-focused

在“自底向上”视图中按自时间(即排除子函数所用时间的总时间)对条目进行排序,我们可以轻松找出那些自己消耗最多时间的函数,进而对这些函数的实现进行进一步的细致检查。同时,我们还需要尽量消除深层次的调用栈,这可以通过查找火焰图中较长的垂直条形找到。

实现高性能

在开发过程中,Cmark 已经使用我们上面展示的性能分析方法进行了数百次性能分析,以追求令人满意的性能。那么,它在与流行的 JavaScript Markdown 库的比较中表现如何呢?

在这个测试中,我们选择了 MicromarkRemark——这两个在 JavaScript 生态系统中广泛使用的 Markdown 库——作为我们的参考。我们在这个测试中使用了最新版本的 Chrome 133 作为我们的 JS 和 Wasm 运行时,并使用 Tinybench 来测量每个库的平均吞吐量。

以下是这些库在 MacBook M1 Pro 上将《CommonMark 规范》转换为 HTML 的平均吞吐量:

测试(从最快到最慢)样本数平均值/Hz±/%
cmark.mbt (WASM-GC + wasm-opt)21203.663.60
cmark.mbt (WASM-GC)19188.463.84
micromark1015.482.07
remark1014.283.16

结果非常明确:得益于持续的性能分析和优化过程,Cmark 现在比 JavaScript 基于的库快了大约 12 倍,相比于 Micromark 快了 13 倍。并且,wasm-opt 的额外优化步骤可以为 Cmark 提供额外的性能提升,将这些因子提高到大约 13 倍和 14 倍。

总之,Cmark 的性能证明了 MoonBit 在前端开发场景中提供可见效率提升的强大能力。

如果你对这个演示的细节感兴趣,可以在 GitHub 上查看最终代码。用于基准测试的代码也可以在 这里 获得。

New to MoonBit?

在 JavaScript 中使用 MoonBit 的高性能 Wasm 库

· 阅读需 6 分钟

封面

在我们之前的一篇博客文章中,我们已经开始探索如何在 MoonBit 的 Wasm GC 后端中直接使用 JavaScript 字符串。正如我们所见,不仅可以用 MoonBit 编写一个兼容 JavaScript 的字符串操作 API,而且在编译为 Wasm 后,生成的产物体积非常小。

然而,您可能会好奇,在更现实的使用场景中,这将会是什么样子。因此,今天我们展示一个更贴近实际的场景:在一个由 JavaScript 驱动的 Web 应用程序中渲染 Markdown 文档,并借助 MoonBit 库 Cmark 和 Wasm 的 JS 字符串内置提案。

动机

Cmark 是一个新的 MoonBit 库,用于处理 Markdown 文档,可以解析纯 CommonMark 和各种常见的 Markdown 语法扩展(如任务列表、脚注、表格等)。此外,它从早期开始就支持外部渲染器,并附带了一个名为 cmark_html 的官方 HTML 渲染器实现。

鉴于 Markdown 在当今网络世界中的广泛存在,将 Markdown 转换为 HTML 的流程仍然是几乎每个 JavaScript 开发者工具箱中的重要工具。因此,这也成为展示 MoonBit Wasm GC API 在前端 JavaScript 中使用的理想场景。

封装 Cmark

为了进行这个演示,先创建一个新的项目目录:

> mkdir cmark-frontend-example

在该目录中,首先创建一个 MoonBit 库 cmarkwrap 来封装 Cmark

> cd cmark-frontend-example && moon new cmarkwrap

这个额外的项目 cmarkwrap 的主要作用是:

  • Cmark 本身不通过 FFI 边界暴露任何 API,这对大多数 MoonBit 库来说是常见的情况;
  • 我们需要从 mooncakes.io 仓库中获取 Cmark 项目,并将其本地编译为 Wasm GC。

cmarkwrap 的结构非常简单:

  • cmark-frontend-example/cmarkwrap/src/lib/moon.pkg.json:

    {
    "import": ["rami3l/cmark/cmark_html"],
    "link": {
    "wasm-gc": {
    "exports": ["render", "result_unwrap", "result_is_ok"],
    "use-js-builtin-string": true
    }
    }
    }

    这个配置基本与之前的博客中介绍的设置相同,为 Wasm GC 目标启用了 use-js-builtin-string 标志,并导出了相关的封装函数。

  • cmark-frontend-example/cmarkwrap/src/lib/wrap.mbt:

    ///|
    typealias RenderResult = Result[String, Error]

    ///|
    pub fn render(md : String) -> RenderResult {
    @cmark_html.render?(md)
    }

    ///|
    pub fn result_unwrap(res : RenderResult) -> String {
    match res {
    Ok(s) => s
    Err(_) => ""
    }
    }

    ///|
    pub fn result_is_ok(res : RenderResult) -> Bool {
    res.is_ok()
    }

    这里是关键部分。render() 函数封装了底层的 @cmark_html.render() 函数,不再是一个抛出异常的函数,而是返回一个 RenderResult 类型。

    然而,由于 RenderResult 是一个 Wasm 对象(而不是数字或字符串),对 JavaScript 来说是不透明的,因此无法直接被 JavaScript 调用者使用。因此,我们还需要在 MoonBit 中提供拆解该类型的方法:result_unwrap()result_is_ok() 函数正是为此目的设计的,它们接受一个 RenderResult 输入。

与 JavaScript 集成

现在是编写项目 Web 部分的时候了。在此阶段,您可以自由选择任何框架或打包工具。本次演示选择在 cmark-frontend-example 目录下初始化一个最小的项目结构,无需额外的运行时依赖。以下是项目的 HTML 和 JS 部分:

  • cmark-frontend-example/index.html:

    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cmark.mbt + JS</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    <link rel="stylesheet" href="/src/style.css" />
    </body>
    </html>

    这个简单的 HTML 文件包含一个 id="app"div,稍后会用作渲染 Markdown 文档的目标。

  • cmark-frontend-example/src/main.js:

    const cmarkwrapWASM = await WebAssembly.instantiateStreaming(
    fetch("../cmarkwrap/target/wasm-gc/release/build/lib/lib.wasm"),
    {}
    {
    builtins: ["js-string"],
    importedStringConstants: "_",
    },
    );
    const { render, result_is_ok, result_unwrap } =
    cmarkwrapWASM.instance.exports;

    function cmarkWASM(md) {
    const res = render(md);
    if (!result_is_ok(res)) {
    throw new Error("cmarkWASM failed to render");
    }
    return result_unwrap(res);
    }

    async function docHTML() {
    const doc = await fetch("../public/tour.md");
    const docText = await doc.text();
    return cmarkWASM(docText);
    }

    document.getElementById("app").innerHTML = await docHTML();

    cmarkwrap 集成到 JavaScript 中相对简单。在 fetch 并加载 Wasm 产物后,可以直接调用封装函数。result_is_ok() 帮助我们判断是否在正常路径上:如果是,我们可以通过 result_unwrap() 解包跨 FFI 边界的 HTML 结果;否则,可以抛出一个 JavaScript 错误。如果一切顺利,我们最终可以将渲染结果填充到 <div id="app"></div> 中。

现在我们可以编译 MoonBit 的 Wasm GC 产物并启动开发服务器:

> moon -C cmarkwrap build --release --target=wasm-gc
> python3 -m http.server

大功告成!您现在可以在浏览器中访问 http://localhost:8000,并查看由 Cmark MoonBit 库渲染的 A Tour of MoonBit for Beginners

演示

您可以在 GitHub 上找到该演示的代码。

了解更多