diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a58010..f57ffd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## wasm-split-cli vfuture -- Add `Options::cost_model` (`--request-cost` and `--load-probability` in the cli). Shared chunks - are merged when that lowers the expected loading cost, which weighs the requests saved against - the extra bytes loaded, and counts data as well as code. With many split points this cuts the - number of chunks one split module loads from hundreds to tens. The web target merges by default, - the bundler target does not. `--request-cost 0` keeps the previous output. +- Add `Options::cost_model` with a bernoulli model (`--cost-model` in the cli). Shared chunks + are merged when that lowers the expected loading time, which weighs the requests saved against + the extra bytes loaded. This tends to cut the number of chunks one split module loads from + hundreds to tens. The default cost model depends on the target. The web target uses the + new cost model, the bundler target keeps the previous output, equivalent to `--cost-model=none`. ## wasm-split-cli v0.2.4 diff --git a/crates/wasm_split_cli/src/bin/wasm-split-cli.rs b/crates/wasm_split_cli/src/bin/wasm-split-cli.rs index edb5d80..64b0a8a 100644 --- a/crates/wasm_split_cli/src/bin/wasm-split-cli.rs +++ b/crates/wasm_split_cli/src/bin/wasm-split-cli.rs @@ -16,12 +16,12 @@ enum CostModel { #[default] Auto, None, - Expected, + Binary, } #[derive(Debug, Args)] -#[command(next_help_heading = "Options for `--cost-model=expected`")] -struct CostModelExpectedOpts { +#[command(next_help_heading = "Options for `--cost-model=binary`")] +struct CostModelBinaryOpts { /// What one more chunk request costs, in downloaded bytes. 0 keeps every chunk as /// split. #[arg(long, default_value_t = 16 * 1024)] @@ -59,7 +59,7 @@ struct Cli { verbose: bool, #[clap(flatten)] - cost_model_exp_opts: CostModelExpectedOpts, + cost_model_bin_opts: CostModelBinaryOpts, } fn main() -> Result<()> { @@ -77,13 +77,15 @@ fn main() -> Result<()> { let mut opts = this::Options::new(&input_wasm); opts.verbose = args.verbose; match args.cost_model.unwrap_or_default() { - CostModel::Expected if args.cost_model_exp_opts.request_cost > 0 => { - opts.cost_model = Some(this::CostModel::Expected(this::BernoulliCost { - request_cost: args.cost_model_exp_opts.request_cost, - load_probability: args.cost_model_exp_opts.load_probability, + CostModel::Binary if args.cost_model_bin_opts.request_cost > 0 => { + opts.cost_model = Some(this::CostModel::Binary(this::BernoulliCost { + request_cost: args.cost_model_bin_opts.request_cost, + load_probability: args.cost_model_bin_opts.load_probability, })) } - CostModel::Expected | CostModel::None => opts.cost_model = Some(this::CostModel::None), + CostModel::Binary | CostModel::None => { + opts.cost_model = Some(this::CostModel::DisableMerging) + } _ => {} } opts.output_dir = &args.output; diff --git a/crates/wasm_split_cli/src/merge.rs b/crates/wasm_split_cli/src/merge.rs index aa28854..cae765f 100644 --- a/crates/wasm_split_cli/src/merge.rs +++ b/crates/wasm_split_cli/src/merge.rs @@ -64,7 +64,7 @@ struct Group { members: Vec, } -/// [`CostModel::Expected`] with both parameters as `f64`. +/// [`CostModel::Binary`] with both parameters as `f64`. struct ExpectedCost { request_cost: f64, load_probability: f64, @@ -144,8 +144,8 @@ pub fn merge_chunks( request_cost, load_probability, } = match cost_model { - CostModel::None => return, - CostModel::Expected(bernoulli_cost) => *bernoulli_cost, + CostModel::DisableMerging => return, + CostModel::Binary(bernoulli_cost) => *bernoulli_cost, }; let perf_span = perf_span!("merge chunks"); let _perf_span = perf_span.enter(); diff --git a/crates/wasm_split_cli/src/options.rs b/crates/wasm_split_cli/src/options.rs index 412408f..c93e217 100644 --- a/crates/wasm_split_cli/src/options.rs +++ b/crates/wasm_split_cli/src/options.rs @@ -74,22 +74,26 @@ pub struct BernoulliCost { #[derive(Debug)] #[non_exhaustive] pub enum CostModel { - /// Keep every chunk as split. - None, - /// Merge chunks when that lowers the expected loading cost. - Expected(BernoulliCost), + /// Keep every chunk as-is, disables chunk merging. This is equivalent to assuming + /// that a chunk load has no cost. + DisableMerging, + /// Merge chunks when that lowers the expected loading cost. Each split module's + /// loading state is modelled as a [binary random variable]. + /// + /// [binary random variable]: https://en.wikipedia.org/wiki/Bernoulli_distribution + Binary(BernoulliCost), } impl OutputTarget { - /// Chunks of the web target are fetched over the network, one request each. - /// A bundler decides on its own how they get loaded. fn default_cost_model(&self) -> CostModel { + // Chunks of the web target are fetched over the network, so we use the bernoulli loading model here. + // A bundler decides on its own how chunks get loaded. match self { - Self::Web(_) => CostModel::Expected(BernoulliCost { - request_cost: 16384, + Self::Web(_) => CostModel::Binary(BernoulliCost { + request_cost: 16 * 1024, load_probability: 0.05, }), - Self::Bundler(_) => CostModel::None, + Self::Bundler(_) => CostModel::DisableMerging, } } } @@ -97,8 +101,8 @@ impl OutputTarget { impl CostModel { pub(crate) fn validate(&self) -> eyre::Result<()> { match self { - Self::None => {} - Self::Expected(model) => { + Self::DisableMerging => {} + Self::Binary(model) => { eyre::ensure!( 0. <= model.load_probability && model.load_probability <= 1., "expected a load probability between 0 and 1, got {}", @@ -147,11 +151,10 @@ pub struct Options<'a> { /// This option is experimental. /// Default: `true` if the `WASM_SPLIT_CLI_ENABLE_DWARF` environment variable is non-empty. pub emit_dwarf: bool, - /// How to merge the shared chunks. - /// - /// The default model is not subject to semver stability guarantees. + /// A cost model under which chunk optimization is performed. /// /// Default: `None`, which picks a default model based on the output target. + /// This default model is not subject to semver stability guarantees. pub cost_model: Option, /// Enables explicit tests for assumptions we make about the input wasm file during integration testing. #[doc(hidden)] diff --git a/crates/wasm_split_cli/tests/no_reloc_stub_end_to_end.rs b/crates/wasm_split_cli/tests/no_reloc_stub_end_to_end.rs index 241f3d3..3fac880 100644 --- a/crates/wasm_split_cli/tests/no_reloc_stub_end_to_end.rs +++ b/crates/wasm_split_cli/tests/no_reloc_stub_end_to_end.rs @@ -265,7 +265,7 @@ fn no_reloc_stub_is_rewritten_and_dangerous_stub_is_raw_copied() { opts.output_dir = tmp.path(); opts.main_out_path = &main_out; // These tests assert where the painter puts symbols, before any merging. - opts.cost_model = Some(CostModel::None); + opts.cost_model = Some(CostModel::DisableMerging); transform(opts).expect("transform succeeds"); diff --git a/crates/wasm_split_cli/tests/shared_data_symbols_end_to_end.rs b/crates/wasm_split_cli/tests/shared_data_symbols_end_to_end.rs index c775791..4004fd5 100644 --- a/crates/wasm_split_cli/tests/shared_data_symbols_end_to_end.rs +++ b/crates/wasm_split_cli/tests/shared_data_symbols_end_to_end.rs @@ -483,7 +483,7 @@ fn split_wasm(wasm: &[u8]) -> Output { opts.output_dir = tmp.path(); opts.main_out_path = &main_out; // These tests assert where the painter puts symbols, before any merging. - opts.cost_model = Some(CostModel::None); + opts.cost_model = Some(CostModel::DisableMerging); let link_path = tmp.path().join(opts.link_name); let output = transform(opts).expect("transform succeeds"); diff --git a/integration/simple/Cargo.toml b/integration/simple/Cargo.toml index 1643ea5..181617b 100644 --- a/integration/simple/Cargo.toml +++ b/integration/simple/Cargo.toml @@ -18,4 +18,4 @@ wasm-bindgen-test.workspace = true tokio.workspace = true [package.metadata.wasm-split-test-runner] -cost-model = "none" +cost-model = "disable-merging" diff --git a/test-runner/src/bin/wasm-split-test-runner.rs b/test-runner/src/bin/wasm-split-test-runner.rs index f9abd9c..b6929ab 100644 --- a/test-runner/src/bin/wasm-split-test-runner.rs +++ b/test-runner/src/bin/wasm-split-test-runner.rs @@ -226,10 +226,10 @@ fn with_perf_tracing(report_dir: &Path, f: impl FnOnce() -> R) -> R { } #[derive(serde::Deserialize, Debug)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "kebab-case")] enum CostModel { Auto, - None, + DisableMerging, } #[derive(serde::Deserialize, Debug)] @@ -268,10 +268,15 @@ fn wasm_split_cli( let split = with_perf_tracing(report_dir, || { wasm_split_cli_support::transform({ let mut split_opts = wasm_split_cli_support::Options::new(&input); - if let Some(CostModel::None) = - options.as_ref().and_then(|opts| opts.cost_model.as_ref()) - { - split_opts.cost_model = Some(wasm_split_cli_support::CostModel::None); + let cost_model = options + .as_ref() + .and_then(|opts| opts.cost_model.as_ref()) + .unwrap_or(&CostModel::Auto); + match cost_model { + CostModel::DisableMerging => { + split_opts.cost_model = Some(wasm_split_cli_support::CostModel::DisableMerging) + } + CostModel::Auto => {} } split_opts.output_dir = dir; split_opts.main_out_path = &main_file;