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

+192 -106
View File
@@ -1,17 +1,16 @@
use super::prerender::pre_render_static_routes;
use super::templates::InfoPlistData;
use crate::wasm_bindgen::WasmBindgen;
use crate::{BuildRequest, Platform};
use crate::{BuildRequest, Platform, WasmOptConfig};
use crate::{Result, TraceSrc};
use anyhow::Context;
use dioxus_cli_opt::{process_file_to, AssetManifest};
use manganis::{AssetOptions, JsAssetOptions};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use std::collections::HashSet;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::atomic::Ordering;
use std::{collections::HashSet, io::Write};
use std::{sync::atomic::AtomicUsize, time::Duration};
use tokio::process::Command;
@@ -326,21 +325,7 @@ impl AppBundle {
// logo.png
// ```
Platform::Web => {
// Run wasm-bindgen and drop its output into the assets folder under "dioxus"
self.build.status_wasm_bindgen_start();
self.run_wasm_bindgen(&self.app.exe.with_extension("wasm"))
.await?;
// Only run wasm-opt if the feature is enabled
// Wasm-opt has an expensive build script that makes it annoying to keep enabled for iterative dev
// We put it behind the "wasm-opt" feature flag so that it can be disabled when iterating on the cli
self.run_wasm_opt(&self.build.exe_dir())?;
// Write the index.html file with the pre-configured contents we got from pre-rendering
std::fs::write(
self.build.root_dir().join("index.html"),
self.prepare_html()?,
)?;
self.bundle_web().await?;
}
// this will require some extra oomf to get the multi architecture builds...
@@ -413,6 +398,7 @@ impl AppBundle {
if keep_bundled_output_paths.contains(canon_path.as_path()) {
return Ok(());
}
// Otherwise, if it is a directory, we need to walk it and remove child files
if path.is_dir() {
for entry in std::fs::read_dir(path)?.flatten() {
@@ -427,11 +413,16 @@ impl AppBundle {
// If it is a file, remove it
tokio::fs::remove_file(path).await?;
}
Ok(())
})
}
tracing::debug!("Removing old assets");
tracing::trace!(
"Keeping bundled output paths: {:#?}",
keep_bundled_output_paths
);
remove_old_assets(&asset_dir, &keep_bundled_output_paths).await?;
// todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't
@@ -441,7 +432,16 @@ impl AppBundle {
for (asset, bundled) in &self.app.assets.assets {
let from = asset.clone();
let to = asset_dir.join(bundled.bundled_path());
tracing::debug!("Copying asset {from:?} to {to:?}");
// prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir
let from_ = from
.strip_prefix(self.build.krate.workspace_dir())
.unwrap_or(from.as_path());
let to_ = from
.strip_prefix(self.build.krate.workspace_dir())
.unwrap_or(to.as_path());
tracing::debug!("Copying asset {from_:?} to {to_:?}");
assets_to_transfer.push((from, to, *bundled.options()));
}
@@ -459,13 +459,17 @@ impl AppBundle {
// Parallel Copy over the assets and keep track of progress with an atomic counter
let progress = self.build.progress.clone();
let ws_dir = self.build.krate.workspace_dir();
// Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
tokio::task::spawn_blocking(move || {
assets_to_transfer
.par_iter()
.try_for_each(|(from, to, options)| {
let processing = started_processing.fetch_add(1, Ordering::SeqCst);
tracing::trace!("Starting asset copy {processing}/{asset_count} from {from:?}");
let from_ = from.strip_prefix(&ws_dir).unwrap_or(from);
tracing::trace!(
"Starting asset copy {processing}/{asset_count} from {from_:?}"
);
let res = process_file_to(options, from, to);
if let Err(err) = res.as_ref() {
@@ -486,8 +490,8 @@ impl AppBundle {
.await
.map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??;
// Remove the wasm bindgen output directory if it exists
_ = std::fs::remove_dir_all(self.build.wasm_bindgen_out_dir());
// // Remove the wasm bindgen output directory if it exists
// _ = std::fs::remove_dir_all(self.build.wasm_bindgen_out_dir());
// Write the version file so we know what version of the optimizer we used
std::fs::write(
@@ -575,6 +579,7 @@ impl AppBundle {
.krate
.should_pre_compress_web_assets(self.build.build.release);
self.build.status_compressing_assets();
let asset_dir = self.build.asset_dir();
tokio::task::spawn_blocking(move || {
crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
@@ -613,115 +618,196 @@ impl AppBundle {
None
}
pub(crate) async fn run_wasm_bindgen(&mut self, input_path: &Path) -> anyhow::Result<()> {
tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen");
/// Bundle the web app
/// - Run wasm-bindgen
/// - Bundle split
/// - Run wasm-opt
/// - Register the .wasm and .js files with the asset system
async fn bundle_web(&mut self) -> Result<()> {
use crate::{wasm_bindgen::WasmBindgen, wasm_opt};
use std::fmt::Write;
let input_path = input_path.to_path_buf();
// Make sure the bindgen output directory exists
// Locate the output of the build files and the bindgen output
// We'll fill these in a second if they don't already exist
let bindgen_outdir = self.build.wasm_bindgen_out_dir();
std::fs::create_dir_all(&bindgen_outdir)?;
let name = self.build.krate.executable_name().to_string();
let keep_debug =
// if we're in debug mode, or we're generating debug symbols, keep debug info
(self.build.krate.config.web.wasm_opt.debug || self.build.build.debug_symbols)
// but only if we're not in release mode
&& !self.build.build.release;
let start = std::time::Instant::now();
let prebindgen = self.app.exe.clone();
let post_bindgen_wasm = self.build.wasm_bindgen_wasm_output_file();
let should_bundle_split = self.build.build.experimental_wasm_split;
let rustc_exe = self.app.exe.with_extension("wasm");
let bindgen_version = self
.build
.krate
.wasm_bindgen_version()
.expect("this should have been checked by tool verification");
// Prepare any work dirs
std::fs::create_dir_all(&bindgen_outdir)?;
// Prepare our configuration
//
// we turn off debug symbols in dev mode but leave them on in release mode (weird!) since
// wasm-opt and wasm-split need them to do better optimizations.
//
// We leave demangling to false since it's faster and these tools seem to prefer the raw symbols.
// todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically.
let will_wasm_opt = (self.build.build.release || self.build.build.experimental_wasm_split)
&& crate::wasm_opt::wasm_opt_available();
let keep_debug = self.build.krate.config.web.wasm_opt.debug
|| self.build.build.debug_symbols
|| self.build.build.experimental_wasm_split
|| !self.build.build.release
|| will_wasm_opt;
let demangle = false;
let wasm_opt_options = WasmOptConfig {
memory_packing: self.build.build.experimental_wasm_split,
debug: self.build.build.debug_symbols,
..self.build.krate.config.web.wasm_opt.clone()
};
// Run wasm-bindgen. Some of the options are not "optimal" but will be fixed up by wasm-opt
//
// There's performance implications here. Running with --debug is slower than without
// We're keeping around lld sections and names but wasm-opt will fix them
// todo(jon): investigate a good balance of wiping debug symbols during dev (or doing a double build?)
self.build.status_wasm_bindgen_start();
tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen");
let start = std::time::Instant::now();
WasmBindgen::new(&bindgen_version)
.input_path(&input_path)
.input_path(&rustc_exe)
.target("web")
.debug(keep_debug)
.demangle(keep_debug)
.demangle(demangle)
.keep_debug(keep_debug)
.remove_name_section(!keep_debug)
.remove_producers_section(!keep_debug)
.out_name(&name)
.keep_lld_sections(true)
.out_name(self.build.krate.executable_name())
.out_dir(&bindgen_outdir)
.remove_name_section(!will_wasm_opt)
.remove_producers_section(!will_wasm_opt)
.run()
.await
.context("Failed to generate wasm-bindgen bindings")?;
// After running wasm-bindgen, add the js and wasm asset to the manifest
let js_output_path = self.build.wasm_bindgen_js_output_file();
let wasm_output_path = self.build.wasm_bindgen_wasm_output_file();
let new_assets = [
(
js_output_path,
AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)),
),
(wasm_output_path, AssetOptions::Unknown),
];
for (asset_path, options) in new_assets {
let hash = manganis_core::hash::AssetHash::hash_file_contents(&asset_path)?;
let output_path_str = asset_path.to_str().ok_or(anyhow::anyhow!(
"Failed to convert wasm bindgen output path to string"
))?;
let bundled_asset = manganis::macro_helpers::create_bundled_asset(
output_path_str,
hash.bytes(),
options,
);
self.app.assets.assets.insert(asset_path, bundled_asset);
}
tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed());
Ok(())
}
// Run bundle splitting if the user has requested it
// It's pretty expensive but because of rayon should be running separate threads, hopefully
// not blocking this thread. Dunno if that's true
if should_bundle_split {
self.build.status_splitting_bundle();
#[allow(unused)]
pub(crate) fn run_wasm_opt(&self, bindgen_outdir: &std::path::Path) -> Result<()> {
if !self.build.build.release {
return Ok(());
};
self.build.status_optimizing_wasm();
if !will_wasm_opt {
return Err(anyhow::anyhow!(
"Bundle splitting requires wasm-opt to be installed or the CLI to be built with `--features optimizations`. Please install wasm-opt and try again."
)
.into());
}
#[cfg(feature = "optimizations")]
{
use crate::config::WasmOptLevel;
// Load the contents of these binaries since we need both of them
// We're going to use the default makeLoad glue from wasm-split
let original = std::fs::read(&prebindgen)?;
let bindgened = std::fs::read(&post_bindgen_wasm)?;
let mut glue = wasm_split_cli::MAKE_LOAD_JS.to_string();
tracing::info!(dx_src = ?TraceSrc::Build, "Running optimization with wasm-opt...");
// Run the emitter
let splitter = wasm_split_cli::Splitter::new(&original, &bindgened);
let modules = splitter
.context("Failed to parse wasm for splitter")?
.emit()
.context("Failed to emit wasm split modules")?;
let mut options = match self.build.krate.config.web.wasm_opt.level {
WasmOptLevel::Z => {
wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively()
}
WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(),
WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(),
WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(),
WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(),
WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(),
WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(),
};
let wasm_file =
bindgen_outdir.join(format!("{}_bg.wasm", self.build.krate.executable_name()));
let old_size = wasm_file.metadata()?.len();
options
// WASM bindgen relies on reference types
.enable_feature(wasm_opt::Feature::ReferenceTypes)
.debug_info(self.build.krate.config.web.wasm_opt.debug)
.run(&wasm_file, &wasm_file)
.map_err(|err| crate::Error::Other(anyhow::anyhow!(err)))?;
// Write the chunks that contain shared imports
// These will be in the format of chunk_0_modulename.wasm - this is hardcoded in wasm-split
tracing::debug!("Writing split chunks to disk");
for (idx, chunk) in modules.chunks.iter().enumerate() {
let path = bindgen_outdir.join(format!("chunk_{}_{}.wasm", idx, chunk.module_name));
wasm_opt::write_wasm(&chunk.bytes, &path, &wasm_opt_options).await?;
writeln!(
glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/assets/{url}\", [], fusedImports);",
url = self
.app
.assets
.register_asset(&path, AssetOptions::Unknown)?.bundled_path(),
)?;
}
let new_size = wasm_file.metadata()?.len();
tracing::debug!(
dx_src = ?TraceSrc::Build,
"wasm-opt reduced WASM size from {} to {} ({:2}%)",
old_size,
new_size,
(new_size as f64 - old_size as f64) / old_size as f64 * 100.0
);
// Write the modules that contain the entrypoints
tracing::debug!("Writing split modules to disk");
for (idx, module) in modules.modules.iter().enumerate() {
let comp_name = module
.component_name
.as_ref()
.context("generated bindgen module has no name?")?;
let path = bindgen_outdir.join(format!("module_{}_{}.wasm", idx, comp_name));
wasm_opt::write_wasm(&module.bytes, &path, &wasm_opt_options).await?;
let hash_id = module.hash_id.as_ref().unwrap();
writeln!(
glue,
"export const __wasm_split_load_{module}_{hash_id}_{comp_name} = makeLoad(\"/assets/{url}\", [{deps}], fusedImports);",
module = module.module_name,
// Again, register this wasm with the asset system
url = self
.app
.assets
.register_asset(&path, AssetOptions::Unknown)?.bundled_path(),
// This time, make sure to write the dependencies of this chunk
// The names here are again, hardcoded in wasm-split - fix this eventually.
deps = module
.relies_on_chunks
.iter()
.map(|idx| format!("__wasm_split_load_chunk_{idx}"))
.collect::<Vec<_>>()
.join(", ")
)?;
}
// Write the js binding
// It's not registered as an asset since it will get included in the main.js file
let js_output_path = bindgen_outdir.join("__wasm_split.js");
std::fs::write(&js_output_path, &glue)?;
// Make sure to write some entropy to the main.js file so it gets a new hash
// If we don't do this, the main.js file will be cached and never pick up the chunk names
let uuid = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, glue.as_bytes());
std::fs::OpenOptions::new()
.append(true)
.open(self.build.wasm_bindgen_js_output_file())
.context("Failed to open main.js file")?
.write_all(format!("/*{uuid}*/").as_bytes())?;
// Write the main wasm_bindgen file and register it with the asset system
// This will overwrite the file in place
// We will wasm-opt it in just a second...
std::fs::write(&post_bindgen_wasm, modules.main.bytes)?;
}
// Make sure to optimize the main wasm file if requested or if bundle splitting
if should_bundle_split || self.build.build.release {
self.build.status_optimizing_wasm();
wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?;
}
// Make sure to register the main wasm file with the asset system
self.app
.assets
.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?;
// Register the main.js with the asset system so it bundles in the snippets and optimizes
self.app.assets.register_asset(
&self.build.wasm_bindgen_js_output_file(),
AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)),
)?;
// Write the index.html file with the pre-configured contents we got from pre-rendering
std::fs::write(
self.build.root_dir().join("index.html"),
self.prepare_html()?,
)?;
Ok(())
}
+12
View File
@@ -23,6 +23,12 @@ impl BuildRequest {
});
}
pub(crate) fn status_splitting_bundle(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::SplittingBundle,
});
}
pub(crate) fn status_start_bundle(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::Bundling {},
@@ -102,6 +108,12 @@ impl BuildRequest {
});
}
pub(crate) fn status_compressing_assets(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::CompressingAssets,
});
}
pub(crate) fn is_server(&self) -> bool {
self.build.platform() == Platform::Server
}
+9 -1
View File
@@ -340,6 +340,14 @@ impl BuildRequest {
cargo_args.push(self.krate.executable_name().to_string());
// the bundle splitter needs relocation data
// we'll trim these out if we don't need them during the bundling process
// todo(jon): for wasm binary patching we might want to leave these on all the time.
if self.build.platform() == Platform::Web && self.build.experimental_wasm_split {
cargo_args.push("--".to_string());
cargo_args.push("-Clink-args=--emit-relocs".to_string());
}
tracing::debug!(dx_src = ?TraceSrc::Build, "cargo args: {:?}", cargo_args);
cargo_args
@@ -658,7 +666,7 @@ impl BuildRequest {
/// Get the path to the wasm bindgen temporary output folder
pub fn wasm_bindgen_out_dir(&self) -> PathBuf {
self.root_dir().join("wasm-bindgen")
self.root_dir().join("wasm")
}
/// Get the path to the wasm bindgen javascript output file
+17 -13
View File
@@ -140,19 +140,23 @@ impl AppBundle {
// If not, insert the script
*html = html.replace(
"</body",
r#"<script>
// We can't use a module script here because we need to start the script immediately when streaming
import("/{base_path}/{js_path}").then(
({ default: init }) => {
init("/{base_path}/{wasm_path}").then((wasm) => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
}
);
</script>
{DX_TOAST_UTILITIES}
r#" <script>
// We can't use a module script here because we need to start the script immediately when streaming
import("/{base_path}/{js_path}").then(
({ default: init, initSync }) => {
// export initSync in case a split module needs to initialize
window.__wasm_split_main_initSync = initSync;
// Actually perform the load
init("/{base_path}/{wasm_path}").then((wasm) => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
}
);
</script>
{DX_TOAST_UTILITIES}
</body"#,
);
+4
View File
@@ -51,6 +51,10 @@ pub(crate) struct BuildArgs {
#[clap(long, default_value_t = true)]
pub(crate) inject_loading_scripts: bool,
/// Experimental: Bundle split the wasm binary into multiple chunks based on `#[wasm_split]` annotations [default: false]
#[clap(long, default_value_t = false)]
pub(crate) experimental_wasm_split: bool,
/// Generate debug symbols for the wasm binary [default: true]
///
/// This will make the binary larger and take longer to compile, but will allow you to debug the
+8 -4
View File
@@ -19,7 +19,7 @@ pub(crate) struct WebConfig {
pub(crate) https: WebHttpsConfig,
/// Whether to enable pre-compression of assets and wasm during a web build in release mode
#[serde(default = "true_bool")]
#[serde(default = "false_bool")]
pub(crate) pre_compress: bool,
/// The wasm-opt configuration
@@ -30,7 +30,7 @@ pub(crate) struct WebConfig {
impl Default for WebConfig {
fn default() -> Self {
Self {
pre_compress: true_bool(),
pre_compress: false_bool(),
app: Default::default(),
https: Default::default(),
wasm_opt: Default::default(),
@@ -58,13 +58,18 @@ pub(crate) struct WasmOptConfig {
/// Keep debug symbols in the wasm file
#[serde(default = "false_bool")]
pub(crate) debug: bool,
/// Enable memory packing
#[serde(default = "false_bool")]
pub(crate) memory_packing: bool,
}
/// The wasm-opt level to use for release web builds [default: 4]
/// The wasm-opt level to use for release web builds [default: Z]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
pub(crate) enum WasmOptLevel {
/// Optimize aggressively for size
#[serde(rename = "z")]
#[default]
Z,
/// Optimize for size
#[serde(rename = "s")]
@@ -83,7 +88,6 @@ pub(crate) enum WasmOptLevel {
Three,
/// Optimize aggressively for speed
#[serde(rename = "4")]
#[default]
Four,
}
+1
View File
@@ -50,6 +50,7 @@ pub(crate) fn pre_compress_folder(path: &Path, pre_compress: bool) -> std::io::R
let entry_path = entry.path();
if entry_path.is_file() {
if pre_compress {
tracing::info!("Pre-compressing file {}", entry_path.display());
if let Err(err) = pre_compress_file(entry_path) {
tracing::error!("Failed to pre-compress file {entry_path:?}: {err}");
}
+15 -6
View File
@@ -68,10 +68,12 @@ impl TraceController {
let filter = if env::var(LOG_ENV).is_ok() {
EnvFilter::from_env(LOG_ENV)
} else if matches!(args.action, Commands::Serve(_)) {
EnvFilter::new("error,dx=trace,dioxus-cli=trace,manganis-cli-support=trace")
EnvFilter::new(
"error,dx=trace,dioxus_cli=trace,manganis_cli_support=trace,wasm_split_cli=trace",
)
} else {
EnvFilter::new(format!(
"error,dx={our_level},dioxus-cli={our_level},manganis-cli-support={our_level}",
"error,dx={our_level},dioxus_cli={our_level},manganis_cli_support={our_level},,wasm_split_cli={our_level}",
our_level = if args.verbosity.verbose {
"debug"
} else {
@@ -131,10 +133,13 @@ impl TraceController {
}
/// Get a handle to the trace controller.
pub fn redirect() -> Self {
pub fn redirect(interactive: bool) -> Self {
let (tui_tx, tui_rx) = unbounded();
TUI_ACTIVE.store(true, Ordering::Relaxed);
TUI_TX.set(tui_tx.clone()).unwrap();
if interactive {
TUI_ACTIVE.store(true, Ordering::Relaxed);
TUI_TX.set(tui_tx.clone()).unwrap();
}
Self { tui_rx }
}
@@ -142,7 +147,11 @@ impl TraceController {
/// Wait for the internal logger to send a message
pub(crate) async fn wait(&mut self) -> ServeUpdate {
use futures_util::StreamExt;
let log = self.tui_rx.next().await.expect("tracer should never die");
let Some(log) = self.tui_rx.next().await else {
return std::future::pending().await;
};
ServeUpdate::TracingLog { log }
}
}
+1
View File
@@ -19,6 +19,7 @@ mod rustc;
mod serve;
mod settings;
mod wasm_bindgen;
mod wasm_opt;
pub(crate) use build::*;
pub(crate) use cli::*;
+1 -1
View File
@@ -760,7 +760,7 @@ We checked the folder: {}
tracing::error!("Failed to push .env file to device: {e}");
}
// eventually, use the user's MainAcitivty, not our MainAcitivty
// eventually, use the user's MainActivity, not our MainActivity
// adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,);
+1 -1
View File
@@ -39,7 +39,7 @@ pub(crate) use watcher::*;
/// to a dynamic one on the fly.
pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
// Redirect all logging the cli logger
let mut tracer = TraceController::redirect();
let mut tracer = TraceController::redirect(args.is_interactive_tty());
// Load the krate and resolve the server args against it - this might log so do it after we turn on the tracer first
let krate = args.load_krate().await?;
+2
View File
@@ -533,6 +533,8 @@ impl Output {
lines.push(krate.as_str().dark_gray())
}
BuildStage::OptimizingWasm {} => lines.push("Optimizing wasm".yellow()),
BuildStage::SplittingBundle {} => lines.push("Splitting bundle".yellow()),
BuildStage::CompressingAssets => lines.push("Compressing assets".yellow()),
BuildStage::PrerenderingRoutes {} => lines.push("Prerendering static routes".yellow()),
BuildStage::RunningBindgen {} => lines.push("Running wasm-bindgen".yellow()),
BuildStage::RunningGradle {} => lines.push("Running gradle assemble".yellow()),
+26 -11
View File
@@ -18,10 +18,11 @@ pub(crate) struct WasmBindgen {
demangle: bool,
remove_name_section: bool,
remove_producers_section: bool,
keep_lld_exports: bool,
}
impl WasmBindgen {
pub fn new(version: &str) -> Self {
pub(crate) fn new(version: &str) -> Self {
Self {
version: version.to_string(),
input_path: PathBuf::new(),
@@ -33,65 +34,73 @@ impl WasmBindgen {
demangle: true,
remove_name_section: false,
remove_producers_section: false,
keep_lld_exports: false,
}
}
pub fn input_path(self, input_path: &Path) -> Self {
pub(crate) fn input_path(self, input_path: &Path) -> Self {
Self {
input_path: input_path.to_path_buf(),
..self
}
}
pub fn out_dir(self, out_dir: &Path) -> Self {
pub(crate) fn out_dir(self, out_dir: &Path) -> Self {
Self {
out_dir: out_dir.to_path_buf(),
..self
}
}
pub fn out_name(self, out_name: &str) -> Self {
pub(crate) fn out_name(self, out_name: &str) -> Self {
Self {
out_name: out_name.to_string(),
..self
}
}
pub fn target(self, target: &str) -> Self {
pub(crate) fn target(self, target: &str) -> Self {
Self {
target: target.to_string(),
..self
}
}
pub fn debug(self, debug: bool) -> Self {
pub(crate) fn debug(self, debug: bool) -> Self {
Self { debug, ..self }
}
pub fn keep_debug(self, keep_debug: bool) -> Self {
pub(crate) fn keep_debug(self, keep_debug: bool) -> Self {
Self { keep_debug, ..self }
}
pub fn demangle(self, demangle: bool) -> Self {
pub(crate) fn demangle(self, demangle: bool) -> Self {
Self { demangle, ..self }
}
pub fn remove_name_section(self, remove_name_section: bool) -> Self {
pub(crate) fn remove_name_section(self, remove_name_section: bool) -> Self {
Self {
remove_name_section,
..self
}
}
pub fn remove_producers_section(self, remove_producers_section: bool) -> Self {
pub(crate) fn remove_producers_section(self, remove_producers_section: bool) -> Self {
Self {
remove_producers_section,
..self
}
}
pub(crate) fn keep_lld_sections(self, keep_lld_sections: bool) -> Self {
Self {
keep_lld_exports: keep_lld_sections,
..self
}
}
/// Run the bindgen command with the current settings
pub async fn run(&self) -> Result<()> {
pub(crate) async fn run(&self) -> Result<()> {
let binary = self.get_binary_path().await?;
let mut args = Vec::new();
@@ -121,6 +130,10 @@ impl WasmBindgen {
args.push("--remove-producers-section");
}
if self.keep_lld_exports {
args.push("--keep-lld-exports");
}
// Out name
args.push("--out-name");
args.push(&self.out_name);
@@ -144,6 +157,8 @@ impl WasmBindgen {
.expect("input_path should be valid utf8");
args.push(input_path);
tracing::debug!("wasm-bindgen args: {:#?}", args);
// Run bindgen
Command::new(binary)
.args(args)
+102
View File
@@ -0,0 +1,102 @@
use crate::config::WasmOptLevel;
use crate::{Result, WasmOptConfig};
use std::path::Path;
#[memoize::memoize(SharedCache)]
pub fn wasm_opt_available() -> bool {
if cfg!(feature = "optimizations") {
return true;
}
which::which("wasm-opt").is_ok()
}
/// Write these wasm bytes with a particular set of optimizations
pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
tokio::fs::write(output_path, bytes).await?;
optimize(output_path, output_path, cfg).await?;
Ok(())
}
#[allow(unreachable_code)]
pub async fn optimize(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
#[cfg(feature = "optimizations")]
return run_from_lib(input_path, output_path, cfg).await;
// It's okay not to run wasm-opt but we should *really* try it
if which::which("wasm-opt").is_err() {
tracing::warn!("wasm-opt not found and CLI is compiled without optimizations. Skipping optimization for {}", input_path.display());
return Ok(());
}
run_locally(input_path, output_path, cfg).await?;
Ok(())
}
async fn run_locally(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
let mut args = vec![
// needed by wasm-bindgen
"--enable-reference-types",
];
if cfg.memory_packing {
// needed for our current approach to bundle splitting to work properly
// todo(jon): emit the main module's data section in chunks instead of all at once
args.push("--memory-packing");
}
if !cfg.debug {
args.push("--strip-debug");
} else {
args.push("--debuginfo");
}
let level = match cfg.level {
WasmOptLevel::Z => "-Oz",
WasmOptLevel::S => "-Os",
WasmOptLevel::Zero => "-O0",
WasmOptLevel::One => "-O1",
WasmOptLevel::Two => "-O2",
WasmOptLevel::Three => "-O3",
WasmOptLevel::Four => "-O4",
};
tokio::process::Command::new("wasm-opt")
.arg(input_path)
.arg(level)
.arg("-o")
.arg(output_path)
.args(args)
.output()
.await?;
Ok(())
}
/// Use the `wasm_opt` crate
#[cfg(feature = "optimizations")]
async fn run_from_lib(
input_path: &Path,
output_path: &Path,
options: &WasmOptConfig,
) -> Result<()> {
let mut level = match options.level {
WasmOptLevel::Z => wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively(),
WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(),
WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(),
WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(),
WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(),
WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(),
WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(),
};
level
.enable_feature(wasm_opt::Feature::ReferenceTypes)
.add_pass(wasm_opt::Pass::MemoryPacking)
.debug_info(options.debug)
.run(input_path, output_path)
.map_err(|err| crate::Error::Other(anyhow::anyhow!(err)))?;
Ok(())
}