Releases: tetratelabs/wazero
v1.0.0-rc.2
wazero v1.0.0-rc.2 is a stabilizing release, and the last version before 1.0 next week.
wazero 1.0.0 will happen at our release party attended by many contributors present at wasmio in Barcelona. While this is our first community meetup, it won't be our last. Please join us to suggest or help organize subsequent events.
Below are a list of changes, notably new is operating system packaging. Read on to get the full story!
Packaging
@mathetake and @evacchi worked together to publish OS artifacts, you can see attached to this release. Most work was needed around windows as MSI installers need to be signed to avoid warnings. We'll begin distributing wazero via homebrew and winget soon, as well.
Tests
Thanks particularly to @codefromthecrypt and @evacchi, wazero is more tested than we were before, and more than any other runtime we are aware of. We've notably closed gaps not just in WASI, but edge cases around windows and GOOS=js. @mathetake stepped in not just in support of tests, but also adding a test flow so we can code in confidence:
Website
wazero already has extensive code documentation, examples, low-level RATIONALE and language guides.
We have work, yet, on high-level and conceptual documentation. To start, @mathetake added a documentation page, covering architecture and some low-level questions. @evacchi also polished the home page, now that we're focusing a lot more on our CLI user base.
Performance
@mathetake has worked relentlessly to improve performance, especially around initialization of modules. This is analogous to startup time. You can see some of the dramatic improvements below:
$ benchstat v1.0.0-rc.1.txt v1.0.0-rc.2.txt
name old time/op new time/op delta
Initialization/interpreter-32 52.9µs ± 2% 36.1µs ± 2% -31.74% (p=0.000 n=28+25)
Initialization/interpreter-multiple-32 39.9µs ± 2% 39.7µs ±13% ~ (p=0.140 n=29+30)
Initialization/compiler-32 32.2µs ± 7% 28.3µs ± 9% -12.09% (p=0.000 n=30+30)
Initialization/compiler-multiple-32 25.3µs ± 5% 24.5µs ± 8% -3.29% (p=0.000 n=30+26)
Compilation/with_extern_cache-32 206µs ± 2% 200µs ± 2% -2.78% (p=0.000 n=29+30)
Compilation/without_extern_cache-32 6.00ms ± 1% 5.94ms ± 1% -0.95% (p=0.000 n=29+30)
name old alloc/op new alloc/op delta
Initialization/interpreter-32 137kB ± 0% 136kB ± 0% -0.35% (p=0.000 n=30+30)
Initialization/interpreter-multiple-32 137kB ± 0% 137kB ± 0% -0.04% (p=0.000 n=27+27)
Initialization/compiler-32 141kB ± 0% 137kB ± 0% -3.09% (p=0.000 n=30+23)
Initialization/compiler-multiple-32 142kB ± 0% 142kB ± 0% -0.03% (p=0.000 n=27+25)
Compilation/with_extern_cache-32 55.6kB ± 0% 54.6kB ± 0% -1.79% (p=0.000 n=29+30)
Compilation/without_extern_cache-32 1.99MB ± 0% 1.99MB ± 0% -0.12% (p=0.000 n=30+30)
name old allocs/op new allocs/op delta
Initialization/interpreter-32 52.0 ± 0% 38.0 ± 0% -26.92% (p=0.000 n=30+30)
Initialization/interpreter-multiple-32 58.0 ± 0% 57.0 ± 0% -1.72% (p=0.000 n=30+30)
Initialization/compiler-32 42.0 ± 0% 38.0 ± 0% -9.52% (p=0.000 n=30+30)
Initialization/compiler-multiple-32 48.0 ± 0% 47.0 ± 0% -2.08% (p=0.000 n=30+30)
Compilation/with_extern_cache-32 1.10k ± 0% 0.98k ± 0% -10.86% (p=0.000 n=27+30)
Compilation/without_extern_cache-32 32.7k ± 0% 32.6k ± 0% -0.37% (p=0.000 n=30+30)
To support future improvements, we no longer allow importing unnamed modules (moduleName=""). This is an edge case allowed by spec, but not used in practice. By disallowing this, future versions of wazero can be considerably faster instantiating anonymous modules than today.
Changes in support of wasm compiled by Go
Some of you know Go builds wasm binaries when the environment variables GOARCH=wasm and GOOS=js
are set. We include an experimental package gojs
which supports this until Go includes a WASI operating system for at least 2 releases. We support gojs
in part due to users who want an alternative runtime besides node.js. The other reason is to help support future development in the Go compiler. Our hope is developers can quickly check behavior between JS and WASI, so that problems are solved quicker.
The main change in this version is moving the gojs directory under the experimental folder. It was always experimental, but via documentation. This should help people know that this operating system is temporary until Go supports WASI (GOOS=wasip1
) for at least 2 releases.
The other change is exposing gojs.Config
with additional go-specific feature toggles, enabled by default in our CLI. This allows things not defined in WASI to pass, for example functionality about the working directory or user IDs of the process. This allows wazero to pass 100pct of the os package tests defined by Go.
wazero v1.0.0-rc.1
wazero v1.0.0-rc.1 starts our journey towards 1.0, with no public API changes. We will have at least one more release candidate between now and 1.0, in a week or two. The next candidate will include CLI binaries, which will allow our non-Go community to use wazero without installing Go, first.
wazero 1.0 will happen March 24th, during the wazero release party at wasmio hosted by Tetrate. Follow Edoardo for updates on on the party, who's doing great job organizing including lightning talks by end users.
Many of you have requested a community page on our website. We added this including a list of users who explicitly opted in. The user list is opt-in, and generally higher signal than the "Used By" list on the GitHub site, as the latter includes transient dependencies.
The biggest internal code change in v1.0.0-rc.1 let to wazero passing the entire wasi-testsuite on linux, darwin (macOS) and windows, with zero exceptions. To do so took tens of days of effort from both @codefromthecrypt and @mathetake, and described at the bottom for the curious. By passing every test defined by WebAssembly, as well stdlib tests in TinyGo and Zig, it is easier for end users to feel confident wazero is a great choice for stability. This is worth the couple weeks of pain.
We intentionally didn't do any other large changes, but there were several people to thank for minor changes. Some of the below took many days of effort each!
- @achille-roussel for refactoring an internal type with generics so we can use it for open directories.
- @ckaznocha for fixing a concurrency issue on context cancellation.
- @mathetake for a mountain of optimizations to reduce compilation overhead
- @dmvolod for removing a redundant wasm decoding validation.
- @evacchi for adding
-timeout duration
to the CLI which stops runaway processes. - @evacchi for fixing a corner case around max memory limit
- @mathetake for allowing the CLI to be built with an external version (for packaging)
In closing, thanks very much for sticking with us this last year and a half almost, leading to 1.0. Our fantastic ecosystem is the reason you have a zero dependency runtime, something you can embed without thinking about work or version clashes with other tools. We've done great work together to bring Go forward in the WebAssembly ecosystem, and people are noticing!
Notes on wasi-testsuite compliance
Most people don't need to read this part, so only do if you are interested in low level details, or bored!
wasi-testsuite compliance means passing tests that verify the expected behaviors of wasi_snapshot_preview1, as decided by the WASI W3C subgroup. It does so by running tests compiled in different languages, currently AssemblyScript, C and Rust.
As described at the beginning of the release notes, wazero v1.0.0-rc.1 passes all tests as doing so is least confusing to the ecosystem. This means passing things we don't advise or agree with. Our former release left out a couple tests due to performance overhead, but we pass them now despite it. If you care about this, read on!
The last pre-release skipped a test on dot and dot-dot entries in directory listings. Go throws away these entries before we can read them. To resurrect them costs tens of microseconds. The cost is fixed overhead around inodes, but this same topic has a large performance footprint when multiplied in directory listings.
Recently, a test was added to require inode data (file serial numbers) inside all directory listings. For example, if your directory includes "wazero.txt", a non-zero inode must be returned
inode data isn't typically used except comparing file identity, and even then requires the device to do so properly. For example, Go has a SameFile function which lazily gets this data, as it is well.. expensive. wasi-libc made a change recently to fetch this data regardless of it it is used or not. Not all compilers use wasi-libc, for example Zig has their own directory logic. However, at least C and Rust do, so mitigating this problem became an issue for us, not just for the spectest, but the underlying logic in wasi-libc. Specifically, compilers that use wasi-libc 17+ will perform a guest side stat fan-out when the ino returned is zero. Doing a guest-side fan out is much more expensive than host side.
This problem is roughly analogous to ReadDir vs Readdir problem in Go. The lower-d version says "Most clients are better served by the more efficient ReadDir method", ultimately due to an internal stat fanout in worst case. Unfortunately, the performance impact is worse than upper vs lower d readdir in go. In windows, the inode information isn't in the FileInfo.Sys
data, so there's an additional N syscalls to get it from somewhere else.
System performance isn't consistent, but compliance with WASI on this is at least in tens of microseconds additional overhead on directory listings, and it is also linear wrt directory size. Not all users will list directories at after configuration time, neither will all find this overhead intolerable even if they did.
In case you are curious, we did discuss these topics at length with the WASI maintainers. The end of it is that this performance overhead is something they feel as a least bad option of choices before them. That said, progress was made, as they changed the next specification, wasi-filesystem to be performant by default, both not requiring dot directories or eager inodes. We're grateful to have been listened to.
If your performance after initialization time is dominated by this, you may of course file an issue to request a non-strict wasi setting. That said, we'd prefer to not make that setting. We have near term plans, likely this summer, to make the whole filesystem behaviour pluggable. In other words, avoiding this cost will be something you can choose to do on your own later, even for the current WASI specs.
If you'd like to discuss more on this or anything else, jump on gophers slack #wazero
channel. Note: You may need an invite to join gophers. If you like what we are doing, please star our repo as folks appreciate it.
wazero v1.0.0-pre.9
wazero v1.0.0-pre.9 integrates Go context to limit execution time running third party code. This is our last API affecting version before 1.0 in March. We'll cut at least one release candidate between now and then.
For those only interested in breaking changes, here's what you need to do:
- Build with minimally Go 1.18
- Rename
Runtime.InstantiateModuleFromBinary
toRuntime.Instantiate
mod, err := r.InstantiateModuleFromBinary(ctx, guestWasm)
mod, err := r.Instantiate(ctx, guestWasm)
Those of you attending wasmio will be able to meet many contributors and end users in person. This conference is timed almost exactly with our 1.0 release, so quite convenient for the community. If interested about in-person and virtual activities around our release, join gophers slack #wazero channel. Note: You may need an invite to join gophers. Regardless, if you like what we are doing, please star our repo as folks appreciate it. Meanwhile, let's dig into this month's changes!
Stop runaways with Go context!
@mathetake led an exciting development, which allows more control of the third-party wasm you run with wazero. Specifically, a cancel or deadline context can now halt potentially endless loops.
Here's an example:
ctx := context.Background()
r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().
// Integrate go context with the WebAssembly runtime
.WithCloseOnContextDone(true))
defer r.Close(ctx)
mod, _ := r.Instantiate(ctx, infiniteLoopWasm)
infiniteLoop := mod.ExportedFunction("infinite_loop")
// Add function-scoped timeouts however you like.
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// Pass that context when calling a function to prevent it from looping.
_, err = infiniteLoop.Call(ctx)
When the context is done before wasm returns, you'll get an ExitError
with one of these codes: ExitCodeContextCanceled
or ExitCodeDeadlineExceeded
It is understood that some would like more features, such as metering or work-based limits. However, we are excited to have the most commonly requested form of function limiting implemented prior to 1.0. Without this last minute spike from @mathetake, it might not have happened, or ended up as an experiment.
Passing all the tests
Besides features, there has been a large amount of effort to pass all available system tests, on darwin, linux and windows. This means that not only do we pass our own integration tests, but also third party ones like so:
wasi-testsuite
wasi-testsuite is an emerging test suite by the custodians of the WASI (system calls for wasm, basically). These include tests compiled from multiple languages including AssemblyScript, C, rust. Runtimes adapt into the suite, so there's one for wazero similar to wasmtime.
With the exception of a minor detail about dot vs dot-dot directory entries (ignored by most wasm compilers), wazero's CLI passes all tests on darwin, linux and windows.
Thanks to @evacchi and @loganek for infrastructure changes on wasi-testsuite which allowed us to be able to test windows. We'd also like to thank @sunfishcode for elaboration on various compatibility points we hit along the way, especially for working these feedback into the next version of WASI.
To pass all the tests meant we had to change from only implementing functions users request, to basically any function accessed by tests. Thanks to @codefromthecrypt and @mathetake for backfilling over ten WASI functions, as well @evacchi @ncruces as well newcomer @egonelbre for helping with various platform support issues.
TinyGo
TinyGo is the de facto Go compiler for WASI. We've had TinyGo tests for a long time, but this is the first time we can execute TinyGo's tests with our CLI using only configuration. These pass on darwin, linux and windows.
@mathetake lead this work, but this also involved support on the TinyGo side, especially to allow another runtime besides wasmtime to execute tests. It is now possible execute tinygo test
in a way that it runs WebAssembly tests with our CLI. This is thanks to recent work by @anuraaga and @codefromthecrypt, supported by our TinyGo champions @deadprogram and @dgryski.
Zig
Zig is a very popular language that compiles to wasm, specifically targeting WASI. Part of supporting Zig are issues like ensuring wazero supports all host functions that it might call (such as fd_readdir
). Other parts are making sure our CLI can execute their system tests, and those tests pass.
A big milestone happened this this month where @mathetake merged a build check that requires zig system tests to pass on darwin, linux and windows. These passing rides on work similar to wasi-testsuite, but also required changes to Zig both requested by us and those done on their own. Special thanks to @brendandburns @evacchi @jedisct1 @Luukdegram for collaborating to ensure things work both in Zig and in wazero before users notice!
Minor Changes
- Adds
Runtime.InstantiateWithConfig
to allow configuration without a compile step. - Adds
ModuleConfig.WithOsyield
to allow users to control behavior of WASIsched_yield
- Adds a CLI flag
-interpreter
to force use of the interpreter engine - Adds a CLI flag
-env-inherit
to propagate ENV to wasm, useful in Docker. - Adds concurrent-instantiation example
wazero v1.0.0-pre.8
wazero v1.0.0-pre.8 adds a filesystem configuration API that supports writes, tested by multiple third-party suites. It also brings CompilationCache out of experimental state, obviating a hard to explain Namespace API. Finally, this adds more logging scopes.
We don't expect any API changes next month, as we prepare for wazero 1.0 in March. Most of the scheduled work will be completing WASI and improving tests, so that our first formal release is trustworthy. Please upgrade to this version and give us feedback on how it's going.
The best way to contact us is to join gophers slack #wazero channel. Note: You may need an invite to join gophers. If you like what we are doing, please star our repo as folks appreciate it. Meanwhile, let's dig into this month's changes!
Writable filesystem support
An exciting change for many is the ability to configure writeable filesystems. To do that, we've added ModuleConfig.WithFSConfig
which has options to mount directories or a fs.FS
such that wasm can access it. Before, we had ModuleConfig.WithFS
, and we'll leave that forever. This will help reduce complexity for simple-case configuration.
Here's an example of how to allow read access to the current directory as the root filesystem, while write access to a different directory as "/tmp"
moduleConfig = wazero.NewModuleConfig().
// Make the current directory read-only accessible to the guest.
WithReadOnlyDirMount(".", "/").
// Make "/tmp/wasm" accessible to the guest as "/tmp".
WithDirMount("/tmp/wasm", "/tmp")
Under the scenes, this maps to appropriate WebAssembly primitives, namely "preopens" for those compiling WASI or a virtual root if GOOS=js
.
Those using the wazero CLI can take advantage of this with mount-based syntax, which looks very similar to Docker.
For example, here's the same configuration via the command line:
$ wazero run -mount=.:/:ro -mount=/tmp/wasm:/tmp ...
Under the scenes is more comprehensive than last time. Those compiling source via WASI or GOOS=js
can take advantage of newly supported system calls, tested on Linux, MacOS and Windows operating systems. Thanks very much to @codefromthecrypt @evacchi @mathetake and @ncruces for collaborating on these!
fd_filestat_set_size
fd_filestat_set_times
fd_sync
fd_tell
fd_pwrite
Some of you may wonder about our progress on a custom filesystem plugin. We have plans to do that, but after version 1.0. This configuration API was designed to be forwards compatible with a raw filesystem plugin once it is ready.
Standard Library Integration Tests
For the first time, wazero change depends on 3rd party integration tests for aspects beyond the WebAssembly Core specification. Specifically, we use multiple tests to ensure WASI not only works based on what the spec leads define, but also work in practice in TinyGo and Zig programming languages. By running multiple tests we are able to get an implicit quorum of what certain functions are expected to do, and reduce the amount of surprise by end users who simply want things to work.
For example, we use wazero instead of wasmtime to run TinyGo wasi target tests. If any fails, our build breaks. We are nearly there with Zig, too, and will be by next month. Both of these are also thanks to the language communities themselves, who have helped champion patches needed to make things portable.
We also run the emerging wasi-testsuite, defined by the spec team. We pass tests they define for the AssemblyScript and C programming languages. We don't yet pass all rust tests recently added from wasmtime: 5 fail mostly due to some edge case functions we've not yet implemented. However, we expect to pass all of them by 1.0 or sooner, or have a very good reason if we don't.
Beyond WASI, we also test the GOARCH=wasm GOOS=js
platform baked into the standard Go sdk. This is typically tested via the node.js runtime (which uses V8), but our CLI works in lieu of that also. As Go considers this experimental, we don't require passing all tests, yet. That said we test each function that also exists in WASI. This helps existing users of GOARCH=wasm GOOS=js
as well paves an easier transition for those working on the upcoming Go WASI proposal.
Getting these tests understood and integrated into our CI took a lot of effort, with special thanks to @achille-roussel @evacchi and @mathetake for their hard work.
Compilation Cache
In previous versions, we had experimental support for compilation cache. This reduces the first-request penalty on cold starts from the same wasm file by re-using work serialized to disk. We've now exported CompilationCache
as a stable API which can be configured by RuntimeConfig
. Along the way, we removed the complicated Namespace
type as the same isolation+performance benefit can happen by sharing a compilation cache between runtimes. Thanks @mathetake for the hard work simplifying the user experience!
Here's an example of this in action:
// Initializes the new compilation cache with the cache directory.
// This allows the compilation caches to be shared even across multiple OS processes.
cache, err := wazero.NewCompilationCacheWithDir(cacheDir)
if err != nil {
log.Panicln(err)
}
defer cache.Close(ctx)
// Creates a shared runtime config to share the cache across multiple wazero.Runtime.
runtimeConfig := wazero.NewRuntimeConfig().WithCompilationCache(cache)
// Creates two wazero.Runtimes with the same compilation cache.
runtimeA := wazero.NewRuntimeWithConfig(ctx, runtimeConfig)
runtimeB := wazero.NewRuntimeWithConfig(ctx, runtimeConfig)
HostLogging Scopes
In this version, we've added several log scopes that can compose together, and give you more insight into what's happening in the third-party code you are loading. Thanks @codefromthecrypt for the continued work on making execution easier to understand.
Here's example CLI output that uses the "exit" and "filesystem" logging scopes.
$ wazero run --hostlogging=exit,filesystem --mount=.:/:ro cat.wasm /not_found.txt
==> wasi_snapshot_preview1.fd_prestat_get(fd=3)
<== (prestat={pr_name_len=1},errno=ESUCCESS)
==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3)
<== (path=/,errno=ESUCCESS)
==> wasi_snapshot_preview1.fd_prestat_get(fd=4)
<== (prestat=,errno=EBADF)
==> wasi_snapshot_preview1.fd_fdstat_get(fd=3)
<== (stat={filetype=DIRECTORY,fdflags=,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=SYMLINK_FOLLOW,path=not_found.txt,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
<== (opened_fd=,errno=ENOENT)
==> wasi_snapshot_preview1.proc_exit(rval=1)
Those programming in Go can also use this, but note that this API is still experimental, so can cause version compatibility problems. Only compile this API if you are able to change that code when upgrading wazero.
loggingCtx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
logging.NewHostLoggingListenerFactory(&log, logging.LogScopeRandom|logging.LogScopeExit))
Other interesting changes
- Thanks @mathetake for various compiler fixes and better fuzzing support
- Thanks @evacchi for adding
RuntimeConfig.WithCustomSections()
, which allows users to inspect sections wazero doesn't use. - Thanks @evacchi for starting the Zig language page https://wazero.io/languages/zig/
wazero v1.0.0-pre.7
wazero v1.0.0-pre.7 fixes a compilation problem affecting those building GOARCH=386
. Other users can skip this version.
wazero v1.0.0-pre.6
wazero v1.0.0-pre.6 improves performance, CLI and adds a writeable filesystem.
Performance, reliability and usability have been recurring trends in wazero this year, and they will continue into next. We're thankful to have so many end users and enthusiasts active, and wow.. it was an active month!
Performance
This December, @mathetake has been working hard on the compiler backend, while @ckaznocha put in time to optimize various parts of module initialization. We've also put many more benchmarks in place including I/O functions like WASI. Results were dramatic.
For example, our benchmark of go compiled to wasm (GOOS=js GOARCH=wasm
) improved almost 25%, while our benchmark for WASI+TinyGo improved compilation time by almost 25% and instantiation time nearly 10%.
While these improvements are great, wazero still won't beat optimizing compilers. The good news is that @mathetake proved it is possible to do JIT-ish compilation and execution in pure Go. Moreover, Tetrate has hired an extremely bright engineer to start working with Takeshi on this problem next month. For those interested in how wazero works today, take a look at this year's GopherCon presentation.
CLI
wazero works with diverse wasm compilers. Many re-use the same libraries to perform tasks such as I/O, like wasi-libc. However, even those who use that routinely drop down to pure wasm functions to optimize or decouple. Moreover, Go's compiler uses its own syscall ABI which is unlike WASI. In order for us to be more efficient, we've significantly enhanced our CLI.
Most notably, we added --hostlogging=filesystem
which can show you what's happening during a command.
$ tinygo build -target wasi -o cat.wasm cat.go
$ echo hello world > /tmp/hello.txt
$ wazero run -mount=/tmp:/ --hostlogging=filesystem cat.wasm /hello.txt
==> wasi_snapshot_preview1.fd_prestat_get(fd=3)
<== (prestat={pr_name_len=1},errno=ESUCCESS)
--snip--
We also detect if you are using Go generated wasm automatically now (GOOS=js GOARCH=wasm
). We support logging on that, even though the functions underneath act wildly differently.
$ GOARCH=wasm GOOS=js go build -o cat.wasm cat.go
$ wazero run -mount=/tmp:/ --hostlogging=filesystem cat.wasm /hello.txt
==> go.syscall/js.valueCall(fs.open(name=/hello.txt,flags=,perm=----------))
<== (err=<nil>,fd=4)
--snip--
We've also added a -cachedir
flag which allows running the same wasm faster the second time. Those building images can use the compile
command first, as that allows you to build cache without actually executing the module.
Ex. This command took almost 140ms as it implied compilation. By precompiling, it executes 100ms quicker.
$ time wazero run -mount=/tmp:/ cat.wasm /hello.txt
hello world
real 0m0.139s
user 0m0.178s
sys 0m0.036s
$ wazero compile -cachedir=$HOME/.wazero cat.wasm
$ time wazero run -cachedir=$HOME/.wazero -mount=/tmp:/ cat.wasm /hello.txt
hello world
real 0m0.038s
user 0m0.018s
sys 0m0.019s
Finally, we added wazero version
command, which does what you think it would. Thanks to @codefromthecrypt and @mathetake for the work here.
Writeable filesystem
Getting filesystem I/O working properly in sandboxed wasm across different compilers is an arduous task. We've had a lot of help this release from @vyskocilm @ncruces and @robbertvanginkel getting various system calls working as they should. Recently, @achille-roussel offered to help with polishing up things to allow users to plug-in their own filesystems in a way that goes beyond the limitations of Go's fs.FS
. Notably, while fs.FS
allows writing existing files, it has no support for routine tasks such as creating, renaming or deleting files.
While we are at least a month, possibly two from a plug-in abstraction, but meanwhile we added a new hook that allows you to opt into write support.
fs, err := writefs.NewDirFS("/work/appA")
if err != nil {
log.Panicln(err)
}
config = wazero.NewModuleConfig().WithFS(fs)
writefs.NewDirFS
currently adds creating, renaming and deleting files, and thanks to @achille-roussel we now have better virtualized file descriptors.
January we will continue to complete syscall support in our internal abstraction. This takes a long time, as we like to do things right, by vetting against multiple compilers such as zig, clang, TinyGo and normal Go. We'll setup as many tests as we can, including running TinyGo and Go's stdlib tests with wazero, and wasi-testsuite. Once all of that is working and documented, we'll expose an pluggable API, even if it means slipping our 1.0 release from end of February to March. Meanwhile, we're thrilled to have @achille-roussel contributing code and guidance.
Getting in touch
The wazero team hangs out in two channels in gophers slack: #wazero and #wazero-dev. Note: You may need an invite to join gophers. If you like what we are doing, please star our repo as folks appreciate it.
We also attend conferences who accept our talks: The next up is that.us covering backend WebAssembly use cases in wazero. Please encourage conferences you like to consider wazero, as it is a unique and useful part of both Go programming and WebAssembly landscape.
Regardless, Happy New Year! Thanks for all the engagement that made 2022 so exciting and productive: we're thrilled to continue that forward.
wazero v1.0.0-pre.5
wazero v1.0.0-pre.5 improves debugability. It also makes some API changes that reduce its surface area.
We did an early release to allow the few folks impacted by API changes to test them prior to our normal month-end release. Most users should wait for v1.0.0-pre.6, which will include better performance, as well the ability to create new files.
On that note, we'll remind we have a gophers slack #wazero
channel for support, updates and conversation! Note: You may need an invite to join gophers. If you like what we are doing, please star our repo as folks appreciate it.
Better stack traces
Those who compile "debug" wasm will appreciate better looking stack traces on error.
Before, a stack trace message looks like:
wasm stack trace:
.runtime._panic(i32)
.c()
.b()
.a()
.main.main()
.runtime.run()
._start()
Now, if the %.wasm
includes DWARF debug info (usually a debug
build), stack traces look a lot more precise:
wasm stack trace:
.runtime._panic(i32)
0x16e2: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_tinygowasm.go:73:6 (inlined)
/opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/panic.go:52:7
.c()
0x1911: /Users/mathetake/wazero/internal/testing/dwarftestdata/testdata/tinygo/main.go:19:7
.b()
0x1901: /Users/mathetake/wazero/internal/testing/dwarftestdata/testdata/tinygo/main.go:14:3
.a()
0x18f7: /Users/mathetake/wazero/internal/testing/dwarftestdata/testdata/tinygo/main.go:9:3
.main.main()
0x18ed: /Users/mathetake/wazero/internal/testing/dwarftestdata/testdata/tinygo/main.go:4:3
.runtime.run()
0x18cc: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/scheduler_none.go:26:10
._start()
0x18b6: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_wasm_wasi.go:22:5
Many thanks to @mathetake for the work on this, which was easier due to standard libraries in Go.
Moreover, we added logging.NewHostLoggingListenerFactory
for those only interested in host function calls. This reduces the volume of logging considerably for those trying to troubleshoot their custom functions.
Thanks to @codefromthecrypt for the work making things easier to understand.
API Changes
As we near 1.0, we made some decisions to cancel some features that either weren't used or aren't worth their performance impact.
Notably, we canceled the ability to change the filesystem without re-instantiating the module (experimental.WithFS
). This was a complicated feature and made function calls slower also. Instead, folks should re-instantiate or otherwise lock the file system if it needs to change dynamically between calls. Feel free to ask us on gophers slack #wazero channel if you need advice.
We also removed the ctx
parameter from fine grained calls such as memory reads. This sounded like a nice design idea to have it, but it introduced overhead and also wasn't really actionable except in host functions. This is because most memory accesses are from native code, which don't use go's context. Instead, we left the ctx
parameter on function calls and lifecycle methods.
wazero v1.0.0-pre.4
wazero v1.0.0-pre.4 focuses on portability, and ultimately led to a couple notable new projects using wazero: mosn and pdfium-cli. We also improved performance and infrastructure needed to develop faster. While we have a lot more to go, this version is significantly better and worth looking at and upgrading. Please star repositories of those who help keep this project working!
We want to recognize a couple end users whose direct efforts made wazero better for everyone. They are without a doubt the MVPs of v1.0.0-pre.4
@ckaznocha began the month optimizing how we instantiate modules, so that they are fast enough for request-scope. Clifton used a scientific approach, showing other people how to do flame graphs to see for themselves the performance before and after. The results were verifiable by benchmarks and also ad-hoc feedback from others in the community. Just fantastic work and a good role model.
@jerbob92 worked very hard to migrate a project which used CGO to wasm for access to PDFium. The end result was a statically compiled Go binary which embeds PDFium compiled to a wasm binary. This solved the install problem as there's no longer a shared library dependency. We are thankful to Jeroen for tens of hours of skilled, dedicated time: he worked night and day not just to fix problems he needed, but also do those in a way others can understand and consume them. This dramatically improved wazero's support of emscripten and WASI, as well meaningful clarifications on both API and processes to improve them. Please reach out and thank @jerbob92 if this benefits you!
Now, let's get to the concrete changes in wazero v1.0.0-pre.4
Much improved WASI
Thanks to @jerbob92, we have more WASI functions implemented than before, notably filesystem in nature: fd_filestat_get
, fd_pread
and fd_readdir
. This means you can implement a command like ls
in wasm and wazero can execute it.
We have better infrastructure to develop WASI functionality. This is helpful because often specifications only have a sentence or two description on the behavior one is to implement. We decided that the most effective way to address the gap is testing at least two real compilers before assuming our implementation is correct. Our first test was implementing ls
in a couple compilers and making sure that must pass on every change.
Build compatibility is now one version behind Go's support policy
Before, we supported exactly the same version policy as Go: latest and one before. As of this release it would mean Go 1.19 and 1.18. We had feedback from mosn that this policy is too aggressive as it isn't viable for users to switch so quickly. In surveying, we found a more common approach is one version behind, so three total versions. We decided to go with this, as it still allows us to use Go 1.18 features when we release 1.0 early next year.
Better host function debugging
Before, we had function listeners, but they only worked in the interpreter engine. Most people auto-select the fastest engine, which is the compiler. This meant more config work to use things like function listeners. Thanks to @mathetake, now anyone can use function listeners (noting this is an experimental API).
For example, the below will show you which WASI calls your code uses, to STDOUT:
// Set context to one that has an experimental listener
ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, logging.NewHostLoggingListenerFactory(os.Stdout))
r := wazero.NewRuntime(ctx)
defer r.Close(ctx) // This closes everything this Runtime created.
...
api.GoModuleFunc
now accepts a stack parameter
Few users will define host functions using the low-level api.GoModuleFunc
API as it is more tricky. We changed the api slightly to allow it to allocate less, which helps performance of built-in functions and advanced third party functions.
type GoModuleFunction interface {
- Call(ctx context.Context, mod Module, params []uint64) []uint64
+ Call(ctx context.Context, mod Module, stack []uint64)
}
Most users will need to do nothing as all code that used this API on GitHub have been updated.
Significant performance optimizations and infrastructure improvements
@ckaznocha made numerous optimizations to reduce cost to initialize a module. These help for users who compile once, but want a fresh module per-request. This is the safest way to execute webassembly functions quickly as it prevents side-effects between calls. @mathetake made numerous compiler improvements and added fuzzing to the default PR checks. He also ensured wazero passes on the latest version of WebAssembly 2.0 spec tests. @anuraaga found a corruption issue we corrected when people cache results, which is now fixed. @robbertvanginkel fixed a reference counting bug affecting users of gojs (which runs wasm built by GOOS=js GOARCH=wasm go build ...
)
Other notable changes
- @codefromthecrypt added
wasi_snapshot_preview1.NewFunctionExporter
to allow changing the WASI module name (e.g. to "wasi_unstable") or overriding built-in functions. - @jerbob92 and @codefromthecrypt added built-in
invoke_xxx
functions for Emscripten. Future change will generate these on demand given a wasm binary. - @mathetake added encode and encode functions for int32 and uint32 to help clarify that
api.Function
result arrays are intended to be decoded. Thanks to @jerbob92 for elaboration
wazero v1.0.0-pre.3
wazero v1.0.0-pre.3 focuses on Application Binary Interface (ABI) developers and performance.
Note: wazero will require minimally Go 1.18 when 1.0 is released early next year.
To recap, an Application Binary Interface (ABI) is a contract between the WebAssembly host and the guest, primarily defining functions each side can import. Your application that embeds wazero is the host, and the guest is %.wasm
which wazero instantiates. There are often performance concerns across this boundary. The latest release of wazero cuts some of this by 1us per call, which can add up for frequently called functions. Thanks very much to @anuraaga @codefromthecrypt and @hafeidejiangyou for the help on the main changes of v1.0.0-pre.3 described below!
A few of us have been working on a new ABI http-handler, and in the process renovated how wazero does host functions, particularly to be much more efficient. Many users will have no impact, because they don't define any custom ABI or host functions. Those who do, minimally need to make a change like this:
ctx := context.Background()
-hello := func() {
+hello := func(context.Context) {
fmt.Fprintln(stdout, "hello!")
}
-_, err := r.NewHostModuleBuilder("env").ExportFunction("hello", hello).Instantiate(ctx, r)
+_, err := r.NewHostModuleBuilder("env").
+ NewFunctionBuilder().WithFunc(hello).Export("hello").
+ Instantiate(ctx, r)
Power users can now use HostFunctionBuilder
to define functions that won't use reflection. There are two choices of interfaces to use depending on if that function needs access to the calling module or not: api.GoFunction
and api.GoModuleFunction
. Here's an example defining one.
builder.WithGoFunction(api.GoFunc(func(ctx context.Context, params []uint64) []uint64 {
x, y := uint32(params[0]), uint32(params[1])
sum := x + y
return []uint64{sum}
}, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
While verbose, functions defined like this work a lot faster. If you are working on a low-level ABI, consider using it. wazero ported all of our internal functions to this approach and are happy with the results. For example, the below have no logic change except how parameters and results are passed.
Before
BenchmarkHostCall/Call
BenchmarkHostCall/Call-16 1000000 1050 ns/op
Benchmark_EnvironGet/environGet
Benchmark_EnvironGet/environGet-16 525492 2224 ns/op
Now
BenchmarkHostCall/Call
BenchmarkHostCall/Call-16 14807203 83.22 ns/op
Benchmark_EnvironGet/environGet
Benchmark_EnvironGet/environGet-16 951690 1054 ns/op
Besides the new function definition syntax, v1.0.0-pre.3 has a few more goodies for ABI developers:
Memory.WriteString
to avoid allocationsfd_write
in WASI special casesio.Discard
for more efficient discarding of stdout or stderr.CompiledModule.ExportedMemories()
is available to see if the guest exported "memory" or not, prior to instantiation.
wazero v1.0.0-pre.2
wazero v1.0.0-pre.2 adds a CLI, simplifies syntax and increases performance. This was the result of a lot of feedback and help and we are grateful for so much attention.
If you like what we are doing, please star our repo as folks appreciate it. As usual, you can expect another release next month end.
Also, don't forget to look at the increasing amount of projects using wazero. Regardless of whether a project is an experiment or a largely re-used tool, we're together moving Go forward in WebAssembly.
wazero CLI
We've had a number of requests for a CLI to run wasm, though no one ever raised an issue about it. The most recent request was from @dgryski and @anuraaga stepped up to implement the first cut.
For example, if you have a calculator, you can run it like this:
$ go run github.com/tetratelabs/wazero/cmd/wazero run testdata/cli.wasm 3 4
result: 7
You can also install as a binary like usual in go:
$ go install github.com/tetratelabs/wazero/cmd/wazero@latest
$ wazero run testdata/cli.wasm 3 4
result: 7
Notably, this also supports basic filesystem mounts in a way familiar to docker users.
$ wazero run -mount=/host/path:/ cat.wasm /test.txt
Hello world!
One limitation is that while you can read or modify existing files, adding new files is not yet supported. Watch this issue for updates, but we'll have this working by next release.
We also understand CLI users may want to discuss features on chat. Please join us on gophers slack #wazero
to talk about this or anything else. Note: You may need an invite to join gophers.
Simplified syntax
@anuraaga noticed some improvements we could make in terms of simplicity, and a few of us including @inkeliz dug deeper to identify rarely used features which could be removed or hidden to improve developer experience. Here are a few diffs of interest:
Defaults to features in WebAssembly Core Specification 2.0 (DRAFT)
While compilers should be conservative when targeting WebAssembly Core features, runtimes should be lenient as otherwise people need to constantly turn on all features. Currently, most folks had to to turn on 2.0 features because compilers such as AssemblyScript and TinyGo use some of them by default. Call sites are simpler for most users as a result:
- rc := wazero.NewRuntimeConfig().WithWasmCore2()
- r := wazero.NewRuntimeWithConfig(ctx, rc)
+ r := wazero.NewRuntime(ctx)
Note: You can still customize whatever features you want like so
features := api.CoreFeaturesV2.SetEnabled(api.CoreFeatureMutableGlobal, false)
rConfig = wazero.NewRuntimeConfig().WithCoreFeatures(features)
MustInstantiate
if all you do is panic on error
Folks setting up infrastructure layer code often know there is no source of conflict. For example, a module name conflict due to someone else adding WASI. Such call sites can now be simplified like so:
- _, err := wasi_snapshot_preview1.Instantiate(ctx, wasm)
- if err != nil {
- panic(err)
- }
+ wasi_snapshot_preview1.MustInstantiate(ctx, wasm)
ModuleBuilder
is now HostModuleBuilder
The most common mistake we saw in practice was people exporting memory from host modules. This memory would never be used as host functions use the caller's memory. We removed the confusing APIs and renamed the type accordingly: host modules only define functions your guest (%.wasm) imports!
- envBuilder := r.NewModuleBuilder("env")
+ envBuilder := r.NewHostModuleBuilder("env")
No more CompileConfig
We originally introduced CompileConfig
to do transformations, and the first was memory limit in nature. Later, we introduced our experimental compilation cache, and noticed that this config was not only never used in open source, but also would make it hard to reason with cache invalidation. We moved relevant settings to RuntimeConfig
and removed CompileConfig
, which stopped this sort of boilerplate:
- if m.compiled, err = r.CompileModule(ctx, guest, wazero.NewCompileConfig()); err != nil {
+ if m.compiled, err = r.CompileModule(ctx, guest); err != nil {
Note: You can still limit memory like so
// Ex. To reduce the largest possible memory size from 4GB to 128KB:
rConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(2)
Moreover, you can change the %.wasm
binary if your defaults aren't correct, via compiler flags or in worst case manipulating it. If the compiler you use for WebAssembly cannot control max memory, consider raising an issue with them to make that possible.
Better performance
When compiling wasm into native code, we formerly had to check function call boundaries twice (value and call frame stacks). This resulted in inefficient performance for code that calls functions a lot. Thanks to a very focused week of effort from @mathetake, the wazero's calling convention is simplified and consolidated into one stack. While your performance may vary, the best case gains for Fibonacci were between 10 and 25% faster depending on architecture.