Skip to main content

2 posts tagged with "JavaScript"

View All Tags

Profiling MoonBit-Generated Wasm using Chrome

· 7 min read

cover

In one of our previous blog posts, we have introduced how to consume a MoonBit-powered Wasm library, Cmark, within frontend JavaScript. In this post, we will explore how to profile the same library directly from the Chrome browser. Hopefully, this will help you gain insights and eventually achieve better overall performance with MoonBit in similar use cases.

For this blog post in particular, we will focus on making minimal changes to our previous "Cmark for frontend" example, so that we may apply Chrome's built-in V8 profiler to Cmark's Wasm code.

Profiling the Cmark library

We can easily refactor the original frontend application to include a new navigation bar with two links, "Demo" and "Profiling". Clicking the first will tell the browser to render the HTML for the original A Tour of MoonBit for Beginners example, and clicking the second will lead it to our new document for profiling. (If you are curious about the actual implementation, you can find a link pointing to the final code towards the end of this post.)

Now we are ready to write some code to actually implement Wasm profiling. Are there any particularities involved in terms of profiling Wasm compared to JavaScript?

As it turns out, we can use the very same APIs for profiling Wasm as we do for JavaScript code. There is an article in the Chromium documentation that describes them in more detail, but in short:

  • When we call console.profile(), the V8 profiler will start recording a CPU performance profile;
  • After that, we can call the performance-critical function we would like to analyze;
  • Finally, when we call console.profileEnd(), the profiler will stop the recording and then visualizes the resulting data in Chrome's Performance Tab.

With that in mind, let's have a look at the actual implementation of our profiling functionality:

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
);
}

As you can see, we have to minimize the scope of the code being executed when the profiler is activated. Thus, the code is written so that the call to the cmarkWASM() function is the only thing within that scope.

On the other hand, we have chosen the 0.31.2 version of the CommonMark Specification (a.k.a. spec.md in the code above) as the input document for the profiling mode. This is notably because of the document's richness particularly in the employment of different Markdown features, in addition to its sheer length which can cause trouble for many Markdown parsers:

> wc -l spec.md  # line count
9756 spec.md
> wc -w spec.md # word count
25412 spec.md

We have reorganized our frontend application so that clicking on the "Profiling" link in the navigation bar will trigger the profileView() function above, giving the following:

profile-tab-stripped

If you have ever dug into performance optimization, this flame graph above should look pretty familiar...

Wait, what are wasm-function[679], wasm-function[367] and so on? How are we supposed to know which function corresponds to which number?

It turns out we need to retain some debug information when building our Wasm artifact. After all, we have been using the following command to build our MoonBit project:

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

... and stripping is the standard behavior of moon when producing a release build.

Fortunately, there is an extra flag we can use to keep the symbol information without having to resort to a slow debug build: --no-strip. Let's rebuild our project with it:

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

Note

Similarly, if we would like to use wasm-opt on the resulting Wasm artifact, we can use the --debuginfo (or -g) flag of wasm-opt to preserve the function names in the optimized output.

With the function names retained, we can finally see what is really going on in the Performance Tab!

profile-tab

Analyzing the Flame Graph

The flame graph as shown in the previous screenshot can provide a nice summary of function calls and their respective execution times in our code. In case you are not familiar with it, the main ideas behind it are as follows:

  • The Y-axis represents the call stack, with the topmost function being the one that was called first;
  • The X-axis represents the time spent in execution, with the width of each box-shaped node corresponding to the total time spent in a certain function call and its children.

Since we are investigating the performance of the Cmark library in particular, we should move downwards and concentrate on the node for @rami3l/cmark/cmark_html.render(). Here, for example, we can clearly see that the execution of render() is divided into two main parts, as represented by two children nodes on the graph:

  • @rami3l/cmark/cmark.Doc::from_string(), which stands for the conversion of the input Markdown document into a syntax tree;
  • @rami3l/cmark/cmark_html.from_doc(), which stands for the rendering of the syntax tree into the final HTML document.

To get a better view, let's highlight the render() node in the flame graph with a single click. This will tell Chrome to update the "Bottom-up" view to show only the functions that are transitively called by render(), and we will get something like the following:

profile-tab-focused

After sorting the items by self time (i.e. total time excluding child time) in the "Bottom-up" view, we can easily find out the functions that have consumed the most time on their own. This suggests that their implementation might be worth a closer look. Meanwhile, we would also want to try eliminating deep call stacks when possible, which can be found by looking for the long vertical bars in the flame graph.

Achieving High Performance

During its development process, Cmark has been profiled hundreds of times using the very method we have seen above in pursuit of satisfactory performance, but how does it actually perform against popular JavaScript Markdown libraries?

For this test, we have chosen Micromark and Remark---two well-known Markdown libraries in the JavaScript ecosystem---as our reference. We have used a recent build of Chrome 133 in this test as our JS and Wasm runtimes, and have imported Tinybench to measure the average throughput of each library.

Below is the average throughput of these libraries converting a single copy of the CommonMark spec to HTML on a MacBook M1 Pro:

Test (Fastest to Slowest)SamplesAverage/Hz±/%
cmark.mbt (WASM-GC + wasm-opt)21203.663.60
cmark.mbt (WASM-GC)19188.463.84
micromark1015.482.07
remark1014.283.16

The results are quite clear: thanks to the continuous profiling-and-optimizing process, Cmark is now significantly faster than both JavaScript-based libraries by a factor of about 12x for Micromark and 13x for Remark. Furthermore, wasm-opt's extra optimization passes can give Cmark another performance boost, bringing these factors up to about 13x and 14x respectively.

In conclusion, the performance of Cmark is a testament to the power of MoonBit in providing visible efficiency improvements in an actual frontend development scenario.

If you are interested in the details of this demo, you may check out the final code on GitHub. The code based used in benchmarking is also available here.

New to MoonBit?

Consuming a High Performance Wasm Library in MoonBit from JavaScript

· 5 min read

cover

In one of our previous blog posts, we have already started exploring the use of JavaScript strings directly within MoonBit's Wasm GC backend. As we have previously seen, not only is it possible to write a JavaScript-compatible string-manipulating API in MoonBit, but once compiled to Wasm, the resulting artifact is impressively tiny in size.

In the meantime, however, you might have wondered what it will look like in a more realistic use case. That is why we are presenting today a more realistic setting of rendering a Markdown document on a JavaScript-powered web application, with the help of the MoonBit library Cmark and Wasm's JS String Builtins Proposal.

Motivation

Cmark is a new MoonBit library for Markdown document processing, which makes it possible to parse both vanilla CommonMark and various common Markdown syntax extensions (task lists, footnotes, tables, etc.) in pure MoonBit. Furthermore, open to external renderers since its early days, it comes with a ready-to-use official HTML renderer implementation known as cmark_html.

Given Markdown's ubiquitous presence in today's cyberspace and the web world in particular, a conversion pipeline from Markdown to HTML remains an important tool in virtually every JavaScript developer's toolbox. As such, it also constitutes a perfect scenario for showcasing the use of MoonBit's Wasm GC APIs in frontend JavaScript.

Wrapping over Cmark

For the sake of this demo, let's start with a new project directory:

> mkdir cmark-frontend-example

In that very directory, we will first create a MoonBit library cmarkwrap that wraps Cmark:

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

This extra project cmarkwrap is required mostly because:

  • Cmark in itself doesn't expose any API across the FFI boundary, which is the common case for most MoonBit libraries;
  • We will need to fetch the Cmark project from the mooncakes.io repository and compile it locally to Wasm GC anyway.

cmarkwrap's structure is simple enough:

  • 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
    }
    }
    }

    This setup is pretty much identical to the one we have seen in the previous blog, with the use-js-builtin-string flag enabled for the Wasm GC target, and the relevant wrapper functions exported.

  • 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()
    }

    This is where things start to get interesting. The render() function is a wrapper of the underlying @cmark_html.render() function that, instead of being a throwing function, returns a RenderResult type.

    Unfortunately, being a Wasm object (instead of a number or a string), a RenderResult is opaque to JavaScript, and thus cannot be directly consumed by our JavaScript caller. As a result, we also need to provide means to destruct that very type from within MoonBit as well: the result_unwrap() and result_is_ok() functions are there exactly for this purpose, and that is why they accept a RenderResult input.

Integrating with JavaScript

Now is the time to write the web part of this project. At this point, you are basically free to choose any framework or bundler you prefer. This demo in particular has chosen to initialize a minimal project skeleton under the cmark-frontend-example directory with no extra runtime dependencies. For now, let's focus on the HTML and JS parts of the project:

  • 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>

    This simple HTML file includes a single div of id="app", which will become the target to which we render the Markdown document later.

  • 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();

    As it turns out, integrating cmarkwrap into JavaScript is fairly straightforward. After fetching and loading the Wasm artifact, we can call the wrapper functions right away. The result_is_ok() function helps us identify if we are on the happy path: if we are, we can unwrap the HTML result across the FFI boundary with result_unwrap(); otherwise, we can throw a JavaScript error. If everything goes as well, we can finally use the aforementioned <div id="app"></div> as our output target.

We can now compile the MoonBit Wasm GC artifact and launch the development server:

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

And voilà! You can now read A Tour of MoonBit for Beginners rendered by the Cmark MoonBit library in a JavaScript frontend application at http://localhost:8000.

demo

You may find the code of this demo on GitHub.

New to MoonBit?