feat: router-based wasm bundle splitting (#3683)

* feat: wasm-split base implementation

* kinda sorta works with the cli

* okay great it actually works

* fix: cache busting for chunks

* cleaned up version

* it works with the router

* wip: migrate to a faster linear solution using id remapping

* add a good harness

* it works completely on the docsite

* the super tight build works too

* make parallelizable

* works on the docsite but not the harness

* light cleanups, fix for both harness and docsite

* make codegen smaller by extract some shared functions to prevent monos

* chunking works, but tbd on tuning heuristics

* extract used module

* wip: cleaning up module

* wip: much better understanding of how imports work

* fix the call graph

* working again...

* wip: thinking about ifuncs

* ifunc approach works

* yes the ifunc approach works!!

* it all works completely even with chunks

* small cleanups

* comments

* actually locate all of the symbols

* clean up final implementation

* clean up tomls, revert some mono changs

* clean up, add some more comments / docs in places

* hoist more packages

* clean up impl a bit more

* clean cli

* move harness to playwright

* update playwright

* extract codgen

* add lazy to components

* Add docs to split loader

* clippy

* typos

* clippy, don't generate splits if not on wasm

* whoops, const

* Typos and docs

* enable optimizations in ci

* add optimizations to ci

* add wasm-split router feature

* parallel playwright?

* fix no case check

* disable pre-compress... think that might be the issue

* better logging, no default pre-compress, caching of shared symbols
* Fix: compressing assets

* fix non-interactive logging for serve

* cache playwright even on failure, use the cli with optimizations

* fix prebuild

* fix playwright webserver
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-02-13 18:54:38 -08:00
1 parent 14eb7597df
commit bdeedc13eb
59 files changed
+4249 -299

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
# This folder contains the wasm-split sub-workspace
wasm-split is a tool that allows you to split a wasm binary into multiple chunks that are lazily loaded on demand.
This workspace is comprised of:
- the harness on which we test against
- the wasm-split user-facing crate
- the wasm-split-macro crate which is used to generate the wasm-split loader
- the wasm-split-cli crate which is imported by the dioxus-cli and used when building the wasm modules
- the wasm-used crate which provides a custom walrus `Used` struct that makes it easier to debug why a emit_wasm might be failing
@@ -0,0 +1,16 @@
[package]
name = "wasm-split-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = { workspace = true }
itertools = { workspace = true }
walrus = { workspace = true, features = ["parallel"]}
wasmparser = { workspace = true }
id-arena = { workspace = true }
rayon = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
tracing = { workspace = true }
clap = { workspace = true, features = ["derive"] }
wasm-used = { path = "../wasm-used" }
@@ -0,0 +1,4 @@
# this folder contains the wasm output for the test harnessa, so ignore it
*.wasm
bindgen/
@@ -0,0 +1,63 @@
// when running the harness we need to make sure to uncommon this out...
export function makeLoad(url, deps, fusedImports, initIt) {
let alreadyLoaded = false;
return async (callbackIndex, callbackData) => {
await Promise.all(deps.map((dep) => dep()));
if (alreadyLoaded) return;
try {
const response = await fetch(url);
const initSync = initIt || window.__wasm_split_main_initSync;
const mainExports = initSync(undefined, undefined);
let imports = {
env: {
memory: mainExports.memory,
},
__wasm_split: {
__indirect_function_table: mainExports.__indirect_function_table,
__stack_pointer: mainExports.__stack_pointer,
__tls_base: mainExports.__tls_base,
memory: mainExports.memory,
},
};
for (let mainExport in mainExports) {
imports["__wasm_split"][mainExport] = mainExports[mainExport];
}
for (let name in fusedImports) {
imports["__wasm_split"][name] = fusedImports[name];
}
let new_exports = await WebAssembly.instantiateStreaming(
response,
imports
);
alreadyLoaded = true;
for (let name in new_exports.instance.exports) {
fusedImports[name] = new_exports.instance.exports[name];
}
if (callbackIndex !== undefined) {
mainExports.__indirect_function_table.get(callbackIndex)(
callbackData,
true
);
}
} catch (e) {
console.error(
"Failed to load wasm-split module",
e,
url,
deps,
fusedImports
);
return;
}
};
}
let fusedImports = {};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,178 @@
use clap::Parser;
use std::path::PathBuf;
use wasm_split_cli::SplitModule;
fn main() {
tracing_subscriber::fmt()
.without_time()
.compact()
.with_env_filter("debug,walrus=info")
.init();
match Commands::parse() {
Commands::Split(split_args) => split(split_args),
Commands::Validate(validate_args) => validate(validate_args),
}
}
#[derive(Parser)]
enum Commands {
/// Split a wasm module into multiple chunks
#[clap(name = "split")]
Split(SplitArgs),
/// Validate the main module of a wasm module
#[clap(name = "validate")]
Validate(ValidateArgs),
}
#[derive(Parser)]
struct SplitArgs {
/// The wasm module emitted by rustc
original: PathBuf,
/// The wasm module emitted by wasm-bindgen
bindgened: PathBuf,
/// The output *directory* to write the split wasm files to
out_dir: PathBuf,
}
fn split(args: SplitArgs) {
let original = std::fs::read(&args.original).expect("failed to read input file");
let bindgened = std::fs::read(&args.bindgened).expect("failed to read input file");
_ = std::fs::remove_dir_all(&args.out_dir);
std::fs::create_dir_all(&args.out_dir).expect("failed to create output dir");
tracing::info!("Building split module");
let module = wasm_split_cli::Splitter::new(&original, &bindgened).unwrap();
let mut chunks = module.emit().unwrap();
// Write out the main module
tracing::info!(
"Writing main module to {}",
args.out_dir.join("main.wasm").display()
);
std::fs::write(args.out_dir.join("main.wasm"), &chunks.main.bytes).unwrap();
// Write the js module
std::fs::write(
args.out_dir.join("__wasm_split.js"),
emit_js(&chunks.chunks, &chunks.modules),
)
.expect("failed to write js module");
for (idx, chunk) in chunks.chunks.iter().enumerate() {
tracing::info!(
"Writing chunk {} to {}",
idx,
args.out_dir
.join(format!("chunk_{}_{}.wasm", idx, chunk.module_name))
.display()
);
std::fs::write(
args.out_dir
.join(format!("chunk_{}_{}.wasm", idx, chunk.module_name)),
&chunk.bytes,
)
.expect("failed to write chunk");
}
for (idx, module) in chunks.modules.iter_mut().enumerate() {
tracing::info!(
"Writing module {} to {}",
idx,
args.out_dir
.join(format!(
"module_{}_{}.wasm",
idx,
module.component_name.as_ref().unwrap()
))
.display()
);
std::fs::write(
args.out_dir.join(format!(
"module_{}_{}.wasm",
idx,
module.component_name.as_ref().unwrap()
)),
&module.bytes,
)
.expect("failed to write chunk");
}
}
fn emit_js(chunks: &[SplitModule], modules: &[SplitModule]) -> String {
use std::fmt::Write;
let mut glue = format!(
r#"import {{ initSync }} from "./main.js";
{}"#,
include_str!("./__wasm_split.js")
);
for (idx, chunk) in chunks.iter().enumerate() {
tracing::debug!("emitting chunk: {:?}", chunk.module_name);
writeln!(
glue,
"export const __wasm_split_load_chunk_{idx} = makeLoad(\"/harness/split/chunk_{idx}_{module}.wasm\", [], fusedImports, initSync);",
module = chunk.module_name
).expect("failed to write to string");
}
// Now write the modules
for (idx, module) in modules.iter().enumerate() {
let deps = module
.relies_on_chunks
.iter()
.map(|idx| format!("__wasm_split_load_chunk_{idx}"))
.collect::<Vec<_>>()
.join(", ");
let hash_id = module.hash_id.as_ref().unwrap();
writeln!(
glue,
"export const __wasm_split_load_{module}_{hash_id}_{cname} = makeLoad(\"/harness/split/module_{idx}_{cname}.wasm\", [{deps}], fusedImports, initSync);",
module = module.module_name,
idx = idx,
cname = module.component_name.as_ref().unwrap(),
deps = deps
)
.expect("failed to write to string");
}
glue
}
#[derive(Parser)]
struct ValidateArgs {
/// The input wasm file to validate
main: PathBuf,
chunks: Vec<PathBuf>,
}
fn validate(args: ValidateArgs) {
let bytes = std::fs::read(&args.main).expect("failed to read input file");
let main_module = walrus::Module::from_buffer(&bytes).unwrap();
for chunk in args.chunks {
let bytes = std::fs::read(chunk).expect("failed to read input file");
let chunk_module = walrus::Module::from_buffer(&bytes).unwrap();
assert!(chunk_module.tables.iter().count() == 1);
for import in chunk_module.imports.iter() {
let matching = main_module.exports.iter().find(|e| e.name == import.name);
let Some(matching) = matching else {
tracing::error!("Could not find matching export for import {import:#?}");
continue;
};
tracing::debug!("import: {:?}", matching.name);
}
}
}
@@ -0,0 +1,15 @@
[package]
name = "wasm-split-macro"
version = "0.1.0"
edition = "2021"
[dependencies]
syn = { workspace = true, features = ["full"] }
base16 = { workspace = true }
digest = { workspace = true }
quote = { workspace = true }
sha2 = { workspace = true }
proc-macro2 = { workspace = true }
[lib]
proc-macro = true
@@ -0,0 +1,226 @@
use proc_macro::TokenStream;
use digest::Digest;
use quote::{format_ident, quote};
use syn::{parse_macro_input, parse_quote, FnArg, Ident, ItemFn, ReturnType, Signature};
#[proc_macro_attribute]
pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
let module_ident = parse_macro_input!(args as Ident);
let item_fn = parse_macro_input!(input as ItemFn);
if item_fn.sig.asyncness.is_none() {
panic!("wasm_split functions must be async. Use a LazyLoader with synchronous functions instead.");
}
let LoaderNames {
split_loader_ident,
impl_import_ident,
impl_export_ident,
load_module_ident,
..
} = LoaderNames::new(item_fn.sig.ident.clone(), module_ident.to_string());
let mut desugard_async_sig = item_fn.sig.clone();
desugard_async_sig.asyncness = None;
desugard_async_sig.output = match &desugard_async_sig.output {
ReturnType::Default => {
parse_quote! { -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>> }
}
ReturnType::Type(_, ty) => {
parse_quote! { -> std::pin::Pin<Box<dyn std::future::Future<Output = #ty>>> }
}
};
let import_sig = Signature {
ident: impl_import_ident.clone(),
..desugard_async_sig.clone()
};
let export_sig = Signature {
ident: impl_export_ident.clone(),
..desugard_async_sig.clone()
};
let default_item = item_fn.clone();
let mut wrapper_sig = item_fn.sig;
wrapper_sig.asyncness = Some(Default::default());
let mut args = Vec::new();
for (i, param) in wrapper_sig.inputs.iter_mut().enumerate() {
match param {
syn::FnArg::Receiver(_) => args.push(format_ident!("self")),
syn::FnArg::Typed(pat_type) => {
let param_ident = format_ident!("__wasm_split_arg_{i}");
args.push(param_ident.clone());
pat_type.pat = Box::new(syn::Pat::Ident(syn::PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: param_ident,
subpat: None,
}));
}
}
}
let attrs = &item_fn.attrs;
let stmts = &item_fn.block.stmts;
quote! {
#[cfg(target_arch = "wasm32")]
#wrapper_sig {
#(#attrs)*
#[allow(improper_ctypes_definitions)]
#[no_mangle]
pub extern "C" #export_sig {
Box::pin(async move { #(#stmts)* })
}
#[link(wasm_import_module = "./__wasm_split.js")]
extern "C" {
#[no_mangle]
fn #load_module_ident (
callback: unsafe extern "C" fn(*const ::std::ffi::c_void, bool),
data: *const ::std::ffi::c_void
);
#[allow(improper_ctypes)]
#[no_mangle]
#import_sig;
}
thread_local! {
static #split_loader_ident: wasm_split::LazySplitLoader = unsafe {
wasm_split::LazySplitLoader::new(#load_module_ident)
};
}
// Initiate the download by calling the load_module_ident function which will kick-off the loader
if !wasm_split::LazySplitLoader::ensure_loaded(&#split_loader_ident).await {
panic!("Failed to load wasm-split module");
}
unsafe { #impl_import_ident( #(#args),* ) }.await
}
#[cfg(not(target_arch = "wasm32"))]
#default_item
}
.into()
}
/// Create a lazy loader for a given function. Meant to be used in statics. Designed for libraries to
/// integrate with.
///
/// ```rust, no_run
/// fn SomeFunction(args: Args) -> Ret {}
///
/// static LOADER: wasm_split::LazyLoader<Args, Ret> = lazy_loader!(SomeFunction);
///
/// LOADER.load().await.call(args)
/// ```
#[proc_macro]
pub fn lazy_loader(input: TokenStream) -> TokenStream {
// We can only accept idents/paths that will be the source function
let sig = parse_macro_input!(input as Signature);
let params = sig.inputs.clone();
let outputs = sig.output.clone();
let Some(FnArg::Typed(arg)) = params.first().cloned() else {
panic!(
"Lazy Loader must define a single input argument to satisfy the LazyLoader signature"
)
};
let arg_ty = arg.ty.clone();
let LoaderNames {
name,
split_loader_ident,
impl_import_ident,
impl_export_ident,
load_module_ident,
..
} = LoaderNames::new(
sig.ident.clone(),
sig.abi
.as_ref()
.and_then(|abi| abi.name.as_ref().map(|f| f.value()))
.expect("abi to be module name")
.to_string(),
);
quote! {
{
#[cfg(target_arch = "wasm32")]
{
#[link(wasm_import_module = "./__wasm_split.js")]
extern "C" {
// The function we'll use to initiate the download of the module
#[no_mangle]
fn #load_module_ident(
callback: unsafe extern "C" fn(*const ::std::ffi::c_void, bool),
data: *const ::std::ffi::c_void,
);
#[allow(improper_ctypes)]
#[no_mangle]
fn #impl_import_ident(arg: #arg_ty) #outputs;
}
#[allow(improper_ctypes_definitions)]
#[no_mangle]
pub extern "C" fn #impl_export_ident(arg: #arg_ty) #outputs {
#name(arg)
}
thread_local! {
static #split_loader_ident: wasm_split::LazySplitLoader = unsafe {
wasm_split::LazySplitLoader::new(#load_module_ident)
};
};
unsafe {
wasm_split::LazyLoader::new(#impl_import_ident, &#split_loader_ident)
}
}
#[cfg(not(target_arch = "wasm32"))]
{
wasm_split::LazyLoader::preloaded(#name)
}
}
}
.into()
}
struct LoaderNames {
name: Ident,
split_loader_ident: Ident,
impl_import_ident: Ident,
impl_export_ident: Ident,
load_module_ident: Ident,
}
impl LoaderNames {
fn new(name: Ident, module: String) -> Self {
let unique_identifier = base16::encode_lower(
&sha2::Sha256::digest(format!("{name} {span:?}", name = name, span = name.span()))
[..16],
);
Self {
split_loader_ident: format_ident!("__wasm_split_loader_{module}"),
impl_export_ident: format_ident!(
"__wasm_split_00___{module}___00_export_{unique_identifier}_{name}"
),
impl_import_ident: format_ident!(
"__wasm_split_00___{module}___00_import_{unique_identifier}_{name}"
),
load_module_ident: format_ident!(
"__wasm_split_load_{module}_{unique_identifier}_{name}"
),
name,
}
}
}
@@ -0,0 +1,8 @@
[package]
name = "wasm-split"
version = "0.1.0"
edition = "2021"
[dependencies]
async-once-cell = { workspace = true, features = ["std"] }
wasm-split-macro = { workspace = true }
+214
View File
@@ -0,0 +1,214 @@
use std::{
cell::Cell,
ffi::c_void,
future::Future,
pin::Pin,
rc::Rc,
task::{Context, Poll, Waker},
thread::LocalKey,
};
pub use wasm_split_macro::{lazy_loader, wasm_split};
pub type Result<T> = std::result::Result<T, SplitLoaderError>;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum SplitLoaderError {
FailedToLoad,
}
impl std::fmt::Display for SplitLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SplitLoaderError::FailedToLoad => write!(f, "Failed to load wasm-split module"),
}
}
}
/// A lazy loader that can be used to load a function from a split out `.wasm` file.
///
/// # Example
///
/// To use the split loader, you must first create the loader using the `lazy_loader` macro. This macro
/// requires the complete signature of the function you want to load. The extern abi string denotes
/// which module the function should be loaded from. If you don't know which module to use, use `auto`
/// and wasm-split will automatically combine all the modules into one.
///
/// ```rust, no_run
/// static LOADER: wasm_split::LazyLoader<Args, Ret> = wasm_split::lazy_loader!(extern "auto" fn SomeFunction(args: Args) -> Ret);
///
/// fn SomeFunction(args: Args) -> Ret {
/// // Implementation
/// }
/// ```
///
/// ## The `#[component(lazy)]` macro
///
/// If you're using wasm-split with Dioxus, the `#[component(lazy)]` macro is provided that wraps
/// the lazy loader with suspense. This means that the component will suspense until its body has
/// been loaded.
///
/// ```rust, ignore
/// fn app() -> Element {
/// rsx! {
/// Suspense {
/// fallback: rsx! { "Loading..." },
/// LazyComponent { abc: 0 }
/// }
/// }
/// }
///
/// #[component(lazy)]
/// fn LazyComponent(abc: i32) -> Element {
/// rsx! {
/// div {
/// "This is a lazy component! {abc}"
/// }
/// }
/// }
/// ```
pub struct LazyLoader<Args, Ret> {
imported: unsafe extern "C" fn(arg: Args) -> Ret,
key: &'static LocalKey<LazySplitLoader>,
}
impl<Args, Ret> LazyLoader<Args, Ret> {
/// Create a new lazy loader from a lazy imported function and a LazySplitLoader
///
/// # Safety
/// This is unsafe because we're taking an arbitrary function pointer and using it as the loader.
/// This function is likely not instantiated when passed here, so it should never be called directly.
#[doc(hidden)]
pub const unsafe fn new(
imported: unsafe extern "C" fn(arg: Args) -> Ret,
key: &'static LocalKey<LazySplitLoader>,
) -> Self {
Self { imported, key }
}
/// Create a new lazy loader that is already resolved.
pub const fn preloaded(f: fn(Args) -> Ret) -> Self {
let imported =
unsafe { std::mem::transmute::<fn(Args) -> Ret, unsafe extern "C" fn(Args) -> Ret>(f) };
thread_local! {
static LAZY: LazySplitLoader = LazySplitLoader::preloaded();
};
Self {
imported,
key: &LAZY,
}
}
/// Load the lazy loader, returning an boolean indicating whether it loaded successfully
pub async fn load(&'static self) -> bool {
*self.key.with(|inner| inner.lazy.clone()).as_ref().await
}
/// Call the lazy loader with the given arguments
pub fn call(&'static self, args: Args) -> Result<Ret> {
let Some(true) = self.key.with(|inner| inner.lazy.try_get().copied()) else {
return Err(SplitLoaderError::FailedToLoad);
};
Ok(unsafe { (self.imported)(args) })
}
}
type Lazy = async_once_cell::Lazy<bool, SplitLoaderFuture>;
type LoadCallbackFn = unsafe extern "C" fn(*const c_void, bool) -> ();
type LoadFn = unsafe extern "C" fn(LoadCallbackFn, *const c_void) -> ();
pub struct LazySplitLoader {
lazy: Pin<Rc<Lazy>>,
}
impl LazySplitLoader {
/// Create a new lazy split loader from a load function that is generated by the wasm-split macro
///
/// # Safety
///
/// This is unsafe because we're taking an arbitrary function pointer and using it as the loader.
/// It is likely not instantiated when passed here, so it should never be called directly.
#[doc(hidden)]
pub unsafe fn new(load: LoadFn) -> Self {
Self {
lazy: Rc::pin(Lazy::new({
SplitLoaderFuture {
loader: Rc::new(SplitLoader {
state: Cell::new(SplitLoaderState::Deferred(load)),
waker: Cell::new(None),
}),
}
})),
}
}
fn preloaded() -> Self {
Self {
lazy: Rc::pin(Lazy::new({
SplitLoaderFuture {
loader: Rc::new(SplitLoader {
state: Cell::new(SplitLoaderState::Completed(true)),
waker: Cell::new(None),
}),
}
})),
}
}
/// Wait for the lazy loader to load
pub async fn ensure_loaded(loader: &'static std::thread::LocalKey<LazySplitLoader>) -> bool {
*loader.with(|inner| inner.lazy.clone()).as_ref().await
}
}
struct SplitLoader {
state: Cell<SplitLoaderState>,
waker: Cell<Option<Waker>>,
}
#[derive(Clone, Copy)]
enum SplitLoaderState {
Deferred(LoadFn),
Pending,
Completed(bool),
}
struct SplitLoaderFuture {
loader: Rc<SplitLoader>,
}
impl Future for SplitLoaderFuture {
type Output = bool;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<bool> {
unsafe extern "C" fn load_callback(loader: *const c_void, success: bool) {
let loader = unsafe { Rc::from_raw(loader as *const SplitLoader) };
loader.state.set(SplitLoaderState::Completed(success));
if let Some(waker) = loader.waker.take() {
waker.wake()
}
}
match self.loader.state.get() {
SplitLoaderState::Deferred(load) => {
self.loader.state.set(SplitLoaderState::Pending);
self.loader.waker.set(Some(cx.waker().clone()));
unsafe {
load(
load_callback,
Rc::<SplitLoader>::into_raw(self.loader.clone()) as *const c_void,
)
};
Poll::Pending
}
SplitLoaderState::Pending => {
self.loader.waker.set(Some(cx.waker().clone()));
Poll::Pending
}
SplitLoaderState::Completed(value) => Poll::Ready(value),
}
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "wasm-used"
edition = "2021"
version.workspace = true
[dependencies]
walrus = { workspace = true, features = ["parallel"] }
id-arena = { workspace = true }
tracing = { workspace = true }
+346
View File
@@ -0,0 +1,346 @@
use std::collections::HashSet;
use id_arena::Id;
use walrus::{ir::*, ExportId};
use walrus::{ConstExpr, Data, DataId, DataKind, Element, ExportItem, Function};
use walrus::{ElementId, ElementItems, ElementKind, Module, RefType, Type, TypeId};
use walrus::{FunctionId, FunctionKind, Global, GlobalId};
use walrus::{GlobalKind, Memory, MemoryId, Table, TableId};
type IdHashSet<T> = HashSet<Id<T>>;
/// Set of all root used items in a wasm module.
#[derive(Debug, Default)]
pub struct Roots {
tables: Vec<TableId>,
funcs: Vec<(FunctionId, Location)>,
globals: Vec<GlobalId>,
memories: Vec<MemoryId>,
data: Vec<DataId>,
elements: Vec<ElementId>,
used: Used,
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum Location {
Start,
Export { export: ExportId },
Table { table: TableId },
Memory { memory: MemoryId },
Global { global: GlobalId },
Data,
Element { element: ElementId },
Code { func: FunctionId },
}
impl Roots {
/// Creates a new set of empty roots.
pub fn new() -> Roots {
Roots::default()
}
/// Adds a new function to the set of roots
pub fn push_func(&mut self, func: FunctionId, from: Location) -> &mut Roots {
if self.used.funcs.insert(func) {
// log::trace!("function is used: {:?}", func);
self.funcs.push((func, from));
}
self
}
/// Adds a new table to the set of roots
pub fn push_table(&mut self, table: TableId) -> &mut Roots {
if self.used.tables.insert(table) {
// log::trace!("table is used: {:?}", table);
self.tables.push(table);
}
self
}
/// Adds a new memory to the set of roots
pub fn push_memory(&mut self, memory: MemoryId) -> &mut Roots {
if self.used.memories.insert(memory) {
// log::trace!("memory is used: {:?}", memory);
self.memories.push(memory);
}
self
}
/// Adds a new global to the set of roots
pub fn push_global(&mut self, global: GlobalId) -> &mut Roots {
if self.used.globals.insert(global) {
// log::trace!("global is used: {:?}", global);
self.globals.push(global);
}
self
}
fn push_data(&mut self, data: DataId) -> &mut Roots {
if self.used.data.insert(data) {
// log::trace!("data is used: {:?}", data);
self.data.push(data);
}
self
}
fn push_element(&mut self, element: ElementId) -> &mut Roots {
if self.used.elements.insert(element) {
// log::trace!("element is used: {:?}", element);
self.elements.push(element);
}
self
}
}
/// Finds the things within a module that are used.
///
/// This is useful for implementing something like a linker's `--gc-sections` so
/// that our emitted `.wasm` binaries are small and don't contain things that
/// are not used.
#[derive(Debug, Default)]
pub struct Used {
/// The module's used tables.
pub tables: IdHashSet<Table>,
/// The module's used types.
pub types: IdHashSet<Type>,
/// The module's used functions.
pub funcs: IdHashSet<Function>,
/// The module's used globals.
pub globals: IdHashSet<Global>,
/// The module's used memories.
pub memories: IdHashSet<Memory>,
/// The module's used passive element segments.
pub elements: IdHashSet<Element>,
/// The module's used passive data segments.
pub data: IdHashSet<Data>,
}
impl Used {
/// Construct a new `Used` set for the given module.
pub fn new(module: &Module, deleted: &HashSet<FunctionId>) -> Used {
// log::debug!("starting to calculate used set");
let mut stack = Roots::default();
// All exports are roots
for export in module.exports.iter() {
match export.item {
ExportItem::Function(f) => stack.push_func(
f,
Location::Export {
export: export.id(),
},
),
ExportItem::Table(t) => stack.push_table(t),
ExportItem::Memory(m) => stack.push_memory(m),
ExportItem::Global(g) => stack.push_global(g),
};
}
// The start function is an implicit root as well
if let Some(f) = module.start {
stack.push_func(f, Location::Start);
}
// Initialization of memories or tables is a side-effectful operation
// because they can be out-of-bounds, so keep all active segments.
for data in module.data.iter() {
if let DataKind::Active { .. } = &data.kind {
stack.push_data(data.id());
}
}
for elem in module.elements.iter() {
match elem.kind {
// Active segments are rooted because they initialize imported
// tables.
ElementKind::Active { table, .. } => {
if module.tables.get(table).import.is_some() {
stack.push_element(elem.id());
}
}
// Declared segments can probably get gc'd but for now we're
// conservative and we root them
ElementKind::Declared => {
stack.push_element(elem.id());
}
ElementKind::Passive => {}
}
}
// // And finally ask custom sections for their roots
// for (_id, section) in module.customs.iter() {
// section.add_gc_roots(&mut stack);
// }
// tracing::info!("Used roots: {:#?}", stack);
// Iteratively visit all items until our stack is empty
while !stack.funcs.is_empty()
|| !stack.tables.is_empty()
|| !stack.memories.is_empty()
|| !stack.globals.is_empty()
|| !stack.data.is_empty()
|| !stack.elements.is_empty()
{
while let Some((f, _loc)) = stack.funcs.pop() {
if deleted.contains(&f) {
let func = module.funcs.get(f);
let name = func
.name
.as_ref()
.cloned()
.unwrap_or_else(|| format!("unknown - {}", f.index()));
// panic!(
// "Found a function that should be deleted but is still used: {:?} - {:?}",
// name, f
// );
tracing::error!(
"Found a function that should be deleted but is still used: {:?} - {:?} - {:?}",
name,
f,
_loc
);
if let Location::Code { func } = _loc {
let func_name = module.funcs.get(func).name.as_ref().unwrap();
tracing::error!("Function {:?} is used by {:?}", f, func_name);
}
// continue;
}
let func = module.funcs.get(f);
stack.used.types.insert(func.ty());
match &func.kind {
FunctionKind::Local(func) => {
let mut visitor = UsedVisitor {
cur_func: f,
stack: &mut stack,
};
dfs_in_order(&mut visitor, func, func.entry_block());
}
FunctionKind::Import(_) => {}
FunctionKind::Uninitialized(_) => unreachable!(),
}
}
while let Some(t) = stack.tables.pop() {
for elem in module.tables.get(t).elem_segments.iter() {
stack.push_element(*elem);
}
}
while let Some(t) = stack.globals.pop() {
match &module.globals.get(t).kind {
GlobalKind::Import(_) => {}
GlobalKind::Local(ConstExpr::Global(global)) => {
stack.push_global(*global);
}
GlobalKind::Local(ConstExpr::RefFunc(func)) => {
stack.push_func(*func, Location::Global { global: t });
}
GlobalKind::Local(ConstExpr::Value(_))
| GlobalKind::Local(ConstExpr::RefNull(_)) => {}
}
}
while let Some(t) = stack.memories.pop() {
for data in &module.memories.get(t).data_segments {
stack.push_data(*data);
}
}
while let Some(d) = stack.data.pop() {
let d = module.data.get(d);
if let DataKind::Active { memory, offset } = &d.kind {
stack.push_memory(*memory);
if let ConstExpr::Global(g) = offset {
stack.push_global(*g);
}
}
}
while let Some(e) = stack.elements.pop() {
let e = module.elements.get(e);
if let ElementItems::Functions(function_ids) = &e.items {
function_ids.iter().for_each(|f| {
stack.push_func(*f, Location::Element { element: e.id() });
});
}
if let ElementItems::Expressions(RefType::Funcref, items) = &e.items {
for item in items {
match item {
ConstExpr::Global(g) => {
stack.push_global(*g);
}
ConstExpr::RefFunc(f) => {
stack.push_func(*f, Location::Element { element: e.id() });
}
_ => {}
}
}
}
if let ElementKind::Active { offset, table } = &e.kind {
if let ConstExpr::Global(g) = offset {
stack.push_global(*g);
}
stack.push_table(*table);
}
}
}
// Wabt seems to have weird behavior where a `data` segment, if present
// even if passive, requires a `memory` declaration. Our GC pass is
// pretty aggressive and if you have a passive data segment and only
// `data.drop` instructions you technically don't need the `memory`.
// Let's keep `wabt` passing though and just say that if there are data
// segments kept, but no memories, then we try to add the first memory,
// if any, to the used set.
if !stack.used.data.is_empty() && stack.used.memories.is_empty() {
if let Some(mem) = module.memories.iter().next() {
stack.used.memories.insert(mem.id());
}
}
stack.used
}
}
struct UsedVisitor<'a> {
cur_func: FunctionId,
stack: &'a mut Roots,
}
impl Visitor<'_> for UsedVisitor<'_> {
fn visit_function_id(&mut self, &func: &FunctionId) {
self.stack.push_func(
func,
Location::Code {
func: self.cur_func,
},
);
}
fn visit_memory_id(&mut self, &m: &MemoryId) {
self.stack.push_memory(m);
}
fn visit_global_id(&mut self, &g: &GlobalId) {
self.stack.push_global(g);
}
fn visit_table_id(&mut self, &t: &TableId) {
self.stack.push_table(t);
}
fn visit_type_id(&mut self, &t: &TypeId) {
self.stack.used.types.insert(t);
}
fn visit_data_id(&mut self, &d: &DataId) {
self.stack.push_data(d);
}
fn visit_element_id(&mut self, &e: &ElementId) {
self.stack.push_element(e);
}
}