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
+4229 -279

No files matched your search

-11
View File
@@ -1,11 +0,0 @@
[profile]
[profile.dioxus-wasm]
inherits = "dev"
opt-level = 2
[profile.dioxus-server]
inherits = "dev"
[profile.dioxus-android]
inherits = "dev"
+1 -3
View File
@@ -211,9 +211,7 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
- name: Prebuild CLI cache-on-failure: "true"
run: |
cargo build --package dioxus-cli --release
- name: Playwright - name: Playwright
working-directory: ./packages/playwright-tests working-directory: ./packages/playwright-tests
run: | run: |
+2
View File
@@ -9,6 +9,8 @@
// "rust-analyzer.check.workspace": true, // "rust-analyzer.check.workspace": true,
// "rust-analyzer.check.workspace": false, // "rust-analyzer.check.workspace": false,
// "rust-analyzer.check.features": "all", // "rust-analyzer.check.features": "all",
// "rust-analyzer.cargo.buildScripts.rebuildOnSave": false,
// "rust-analyzer.check.workspace": false,
"rust-analyzer.cargo.features": "all", "rust-analyzer.cargo.features": "all",
"rust-analyzer.check.features": "all", "rust-analyzer.check.features": "all",
// "rust-analyzer.check.allTargets": true, // "rust-analyzer.check.allTargets": true,
Generated
+262 -30
View File
@@ -8,7 +8,7 @@ version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [ dependencies = [
"gimli", "gimli 0.31.1",
] ]
[[package]] [[package]]
@@ -583,6 +583,7 @@ dependencies = [
"brotli 7.0.0", "brotli 7.0.0",
"flate2", "flate2",
"futures-core", "futures-core",
"futures-io",
"memchr", "memchr",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
@@ -648,6 +649,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-once-cell"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
[[package]] [[package]]
name = "async-process" name = "async-process"
version = "2.3.0" version = "2.3.0"
@@ -1504,6 +1511,12 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "base16"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
[[package]] [[package]]
name = "base16ct" name = "base16ct"
version = "0.1.1" version = "0.1.1"
@@ -3407,6 +3420,7 @@ dependencies = [
"dioxus", "dioxus",
"dioxus-cli-config", "dioxus-cli-config",
"dioxus-config-macro", "dioxus-config-macro",
"dioxus-config-macros",
"dioxus-core", "dioxus-core",
"dioxus-core-macro", "dioxus-core-macro",
"dioxus-desktop", "dioxus-desktop",
@@ -3432,6 +3446,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"warnings", "warnings",
"wasm-split",
] ]
[[package]] [[package]]
@@ -3518,6 +3533,7 @@ dependencies = [
"log", "log",
"manganis", "manganis",
"manganis-core", "manganis-core",
"memoize",
"notify", "notify",
"object 0.36.5", "object 0.36.5",
"once_cell", "once_cell",
@@ -3554,6 +3570,7 @@ dependencies = [
"uuid", "uuid",
"walkdir", "walkdir",
"wasm-opt", "wasm-opt",
"wasm-split-cli",
"which 7.0.1", "which 7.0.1",
] ]
@@ -3576,6 +3593,7 @@ dependencies = [
"image", "image",
"imagequant", "imagequant",
"lightningcss", "lightningcss",
"manganis",
"manganis-core", "manganis-core",
"mozjpeg", "mozjpeg",
"object 0.36.5", "object 0.36.5",
@@ -3630,6 +3648,10 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "dioxus-config-macros"
version = "0.6.3"
[[package]] [[package]]
name = "dioxus-core" name = "dioxus-core"
version = "0.6.3" version = "0.6.3"
@@ -3810,6 +3832,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"wasm-split",
"web-time", "web-time",
"wgpu", "wgpu",
] ]
@@ -4110,9 +4133,12 @@ dependencies = [
name = "dioxus-router-macro" name = "dioxus-router-macro"
version = "0.6.3" version = "0.6.3"
dependencies = [ dependencies = [
"base16",
"digest",
"dioxus", "dioxus",
"proc-macro2", "proc-macro2",
"quote", "quote",
"sha2",
"slab", "slab",
"syn 2.0.90", "syn 2.0.90",
] ]
@@ -4799,6 +4825,12 @@ dependencies = [
"zune-inflate", "zune-inflate",
] ]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]] [[package]]
name = "faster-hex" name = "faster-hex"
version = "0.9.0" version = "0.9.0"
@@ -5430,6 +5462,17 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
dependencies = [
"fallible-iterator",
"indexmap 1.9.3",
"stable_deref_trait",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.31.1"
@@ -6150,6 +6193,7 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [ dependencies = [
"ahash 0.8.11", "ahash 0.8.11",
"allocator-api2", "allocator-api2",
"serde",
] ]
[[package]] [[package]]
@@ -6161,6 +6205,7 @@ dependencies = [
"allocator-api2", "allocator-api2",
"equivalent", "equivalent",
"foldhash", "foldhash",
"serde",
] ]
[[package]] [[package]]
@@ -6699,6 +6744,15 @@ dependencies = [
"syn 2.0.90", "syn 2.0.90",
] ]
[[package]]
name = "id-arena"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
dependencies = [
"rayon",
]
[[package]] [[package]]
name = "idea" name = "idea"
version = "0.5.1" version = "0.5.1"
@@ -7103,6 +7157,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.8" version = "0.4.8"
@@ -7177,9 +7240,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.76" version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -7404,6 +7467,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "leb128"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]] [[package]]
name = "lebe" name = "lebe"
version = "0.5.2" version = "0.5.2"
@@ -7728,6 +7797,15 @@ dependencies = [
"imgref", "imgref",
] ]
[[package]]
name = "lru"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a"
dependencies = [
"hashbrown 0.12.3",
]
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.10.1" version = "0.10.1"
@@ -7900,6 +7978,29 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "memoize"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8d1d5792299bab3f8b5d88d1b7a7cb50ad7ef039a8c4d45a6b84880a6526276"
dependencies = [
"lazy_static",
"lru 0.7.8",
"memoize-inner",
]
[[package]]
name = "memoize-inner"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd8f89255d8ff313afabed9a3c83ef0993cc056679dfd001f5111a026f876f7"
dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "metal" name = "metal"
version = "0.27.0" version = "0.27.0"
@@ -8658,7 +8759,7 @@ dependencies = [
"flate2", "flate2",
"memchr", "memchr",
"ruzstd 0.7.3", "ruzstd 0.7.3",
"wasmparser", "wasmparser 0.218.0",
] ]
[[package]] [[package]]
@@ -10958,9 +11059,9 @@ checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.216" version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@@ -11001,9 +11102,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.216" version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -11012,9 +11113,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.133" version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [ dependencies = [
"itoa 1.0.14", "itoa 1.0.14",
"memchr", "memchr",
@@ -13853,6 +13954,35 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "walrus"
version = "0.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6481311b98508f4bc2d0abbfa5d42172e7a54b4b24d8f15e28b0dc650be0c59f"
dependencies = [
"anyhow",
"gimli 0.26.2",
"id-arena",
"leb128",
"log",
"rayon",
"walrus-macro",
"wasm-encoder",
"wasmparser 0.214.0",
]
[[package]]
name = "walrus-macro"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ad39ff894c43c9649fa724cdde9a6fc50b855d517ef071a93e5df82fe51d3"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -13904,20 +14034,21 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@@ -13929,9 +14060,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.49" version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -13942,9 +14073,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -13952,9 +14083,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -13965,19 +14096,21 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"
version = "0.3.49" version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d44563646eb934577f2772656c7ad5e9c90fac78aa8013d776fcdaf24625d" checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"minicov", "minicov",
"scoped-tls",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-bindgen-test-macro", "wasm-bindgen-test-macro",
@@ -13985,15 +14118,24 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-test-macro" name = "wasm-bindgen-test-macro"
version = "0.3.49" version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54171416ce73aa0b9c377b51cc3cb542becee1cd678204812e8392e5b0e4a031" checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.90", "syn 2.0.90",
] ]
[[package]]
name = "wasm-encoder"
version = "0.214.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff694f02a8d7a50b6922b197ae03883fbf18cdb2ae9fbee7b6148456f5f44041"
dependencies = [
"leb128",
]
[[package]] [[package]]
name = "wasm-opt" name = "wasm-opt"
version = "0.116.1" version = "0.116.1"
@@ -14034,6 +14176,60 @@ dependencies = [
"cxx-build", "cxx-build",
] ]
[[package]]
name = "wasm-split"
version = "0.1.0"
dependencies = [
"async-once-cell",
"wasm-split-macro",
]
[[package]]
name = "wasm-split-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"id-arena",
"itertools 0.14.0",
"rayon",
"tracing",
"tracing-subscriber",
"walrus",
"wasm-used",
"wasmparser 0.225.0",
]
[[package]]
name = "wasm-split-harness"
version = "0.1.0"
dependencies = [
"anyhow",
"async-compression",
"dioxus",
"dioxus-router",
"futures",
"getrandom 0.2.15",
"js-sys",
"once_cell",
"reqwest 0.12.9",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "wasm-split-macro"
version = "0.1.0"
dependencies = [
"base16",
"digest",
"proc-macro2",
"quote",
"sha2",
"syn 2.0.90",
]
[[package]] [[package]]
name = "wasm-streams" name = "wasm-streams"
version = "0.4.2" version = "0.4.2"
@@ -14047,6 +14243,29 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "wasm-used"
version = "0.6.3"
dependencies = [
"id-arena",
"tracing",
"walrus",
]
[[package]]
name = "wasmparser"
version = "0.214.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5309c1090e3e84dad0d382f42064e9933fdaedb87e468cc239f0eabea73ddcb6"
dependencies = [
"ahash 0.8.11",
"bitflags 2.6.0",
"hashbrown 0.14.5",
"indexmap 2.7.0",
"semver 1.0.23",
"serde",
]
[[package]] [[package]]
name = "wasmparser" name = "wasmparser"
version = "0.218.0" version = "0.218.0"
@@ -14057,10 +14276,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "web-sys" name = "wasmparser"
version = "0.3.76" version = "0.225.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" checksum = "36e5456165f81e64cb9908a0fe9b9d852c2c74582aa3fe2be3c2da57f937d3ae"
dependencies = [
"bitflags 2.6.0",
"hashbrown 0.15.2",
"indexmap 2.7.0",
"semver 1.0.23",
"serde",
]
[[package]]
name = "web-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
+46 -8
View File
@@ -65,19 +65,19 @@ members = [
"packages/const-serialize-macro", "packages/const-serialize-macro",
"packages/dx-wire-format", "packages/dx-wire-format",
"packages/logger", "packages/logger",
"packages/config-macros",
# Playwright tests
"packages/playwright-tests/liveview",
"packages/playwright-tests/web",
"packages/playwright-tests/fullstack",
"packages/playwright-tests/suspense-carousel",
"packages/playwright-tests/nested-suspense",
# manganis # manganis
"packages/manganis/manganis", "packages/manganis/manganis",
"packages/manganis/manganis-core", "packages/manganis/manganis-core",
"packages/manganis/manganis-macro", "packages/manganis/manganis-macro",
# wasm-split
"packages/wasm-split/wasm-split",
"packages/wasm-split/wasm-split-macro",
"packages/wasm-split/wasm-split-cli",
"packages/wasm-split/wasm-used",
# Full project examples # Full project examples
"example-projects/fullstack-hackernews", "example-projects/fullstack-hackernews",
"example-projects/ecommerce-site", "example-projects/ecommerce-site",
@@ -101,6 +101,7 @@ members = [
"packages/playwright-tests/suspense-carousel", "packages/playwright-tests/suspense-carousel",
"packages/playwright-tests/nested-suspense", "packages/playwright-tests/nested-suspense",
"packages/playwright-tests/cli-optimization", "packages/playwright-tests/cli-optimization",
"packages/playwright-tests/wasm-split-harness",
] ]
[workspace.package] [workspace.package]
@@ -142,14 +143,22 @@ dioxus-fullstack = { path = "packages/fullstack", version = "0.6.2" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.6.2", default-features = false } dioxus_server_macro = { path = "packages/server-macro", version = "0.6.2", default-features = false }
dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.6.2" } dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.6.2" }
dioxus-logger = { path = "packages/logger", version = "0.6.2" } dioxus-logger = { path = "packages/logger", version = "0.6.2" }
dioxus-config-macros = { path = "packages/config-macros", version = "0.6.3" }
const-serialize = { path = "packages/const-serialize", version = "0.6.2" } const-serialize = { path = "packages/const-serialize", version = "0.6.2" }
const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.6.2" } const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.6.2" }
generational-box = { path = "packages/generational-box", version = "0.6.2" } generational-box = { path = "packages/generational-box", version = "0.6.2" }
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.2" } lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.2" }
manganis = { path = "packages/manganis/manganis", version = "0.6.2" } manganis = { path = "packages/manganis/manganis", version = "0.6.2" }
manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.2" } manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.2" }
manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.2" } manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.2" }
wasm-split = { path = "packages/wasm-split/wasm-split", version = "0.1.0" }
wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.1.0" }
wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.1.0" }
wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.1.0" }
warnings = { version = "0.2.1" } warnings = { version = "0.2.1" }
# a fork of pretty please for tests - let's get off of this if we can! # a fork of pretty please for tests - let's get off of this if we can!
@@ -225,6 +234,17 @@ cargo_metadata = "0.18.1"
parking_lot = "0.12.1" parking_lot = "0.12.1"
tracing-wasm = "0.2.1" tracing-wasm = "0.2.1"
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
base16 = "0.2.1"
digest = "0.10.7"
sha2 = "0.10.8"
walrus = { version = "0.23.2", features = ["parallel"] }
id-arena = "2.2.1"
async-compression = { version = "0.4.8", features = ["futures-io", "gzip", "brotli"] }
getrandom = { version = "0.2" }
async-once-cell = { version = "0.5.3" }
rayon = "1.2.0"
wasmparser = "0.225.0"
itertools = "0.14.0"
# desktop # desktop
wry = { version = "0.45.0", default-features = false } wry = { version = "0.45.0", default-features = false }
@@ -250,7 +270,24 @@ debug = 0
# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental # when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental
[profile.release] [profile.release]
incremental = true incremental = true
debug = 0
# crank up the opt level for wasm-split-cli in dev mode
# important here that lto is on and the debug symbols are presenta (since they're used by wasm-opt)a
[profile.wasm-split-release]
inherits = "release"
opt-level = 'z'
lto = true
debug=true
# a profile for running the CLI that's also incremental
[profile.cli-release-dev]
inherits = "release"
opt-level = 3
incremental = true
# crank up walrus since it's quite slow in dev mode
[profile.dev.package.walrus]
opt-level = 3
[profile.release-max-opt] [profile.release-max-opt]
inherits = "release" inherits = "release"
@@ -296,6 +333,7 @@ base64 = { workspace = true, optional = true }
http-range = { version = "0.1.5", optional = true } http-range = { version = "0.1.5", optional = true }
wgpu = { version = "0.19", optional = true } wgpu = { version = "0.19", optional = true }
ouroboros = { version = "*", optional = true } ouroboros = { version = "*", optional = true }
wasm-split = { workspace = true }
[dev-dependencies] [dev-dependencies]
dioxus = { workspace = true, features = ["router"] } dioxus = { workspace = true, features = ["router"] }
+1
View File
@@ -11,6 +11,7 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
manganis = { workspace = true }
manganis-core = { workspace = true } manganis-core = { workspace = true }
object = {version="0.36.0", features=["wasm"]} object = {version="0.36.0", features=["wasm"]}
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
+21
View File
@@ -25,6 +25,27 @@ pub struct AssetManifest {
} }
impl AssetManifest { impl AssetManifest {
/// Manually add an asset to the manifest
pub fn register_asset(
&mut self,
asset_path: &Path,
options: manganis::AssetOptions,
) -> anyhow::Result<BundledAsset> {
let hash = manganis_core::hash::AssetHash::hash_file_contents(asset_path)
.context("Failed to hash file")?;
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.assets.insert(asset_path.into(), bundled_asset);
Ok(bundled_asset)
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn load_from_file(path: &Path) -> anyhow::Result<Self> { pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
let src = std::fs::read_to_string(path)?; let src = std::fs::read_to_string(path)?;
+2
View File
@@ -23,6 +23,7 @@ dioxus-cli-config = { workspace = true }
dioxus-cli-opt = { workspace = true } dioxus-cli-opt = { workspace = true }
dioxus-fullstack = { workspace = true } dioxus-fullstack = { workspace = true }
dioxus-dx-wire-format = { workspace = true } dioxus-dx-wire-format = { workspace = true }
wasm-split-cli = { workspace = true }
clap = { workspace = true, features = ["derive", "cargo"] } clap = { workspace = true, features = ["derive", "cargo"] }
convert_case = { workspace = true } convert_case = { workspace = true }
@@ -126,6 +127,7 @@ tar = "0.4.43"
local-ip-address = "0.6.3" local-ip-address = "0.6.3"
dircpy = "0.3.19" dircpy = "0.3.19"
plist = "1.7.0" plist = "1.7.0"
memoize = "0.5.1"
[build-dependencies] [build-dependencies]
built = { version = "=0.7.4", features = ["git2"] } built = { version = "=0.7.4", features = ["git2"] }
+193 -107
View File
@@ -1,17 +1,16 @@
use super::prerender::pre_render_static_routes; use super::prerender::pre_render_static_routes;
use super::templates::InfoPlistData; use super::templates::InfoPlistData;
use crate::wasm_bindgen::WasmBindgen; use crate::{BuildRequest, Platform, WasmOptConfig};
use crate::{BuildRequest, Platform};
use crate::{Result, TraceSrc}; use crate::{Result, TraceSrc};
use anyhow::Context; use anyhow::Context;
use dioxus_cli_opt::{process_file_to, AssetManifest}; use dioxus_cli_opt::{process_file_to, AssetManifest};
use manganis::{AssetOptions, JsAssetOptions}; use manganis::{AssetOptions, JsAssetOptions};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use std::collections::HashSet;
use std::future::Future; use std::future::Future;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::pin::Pin; use std::pin::Pin;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::{collections::HashSet, io::Write};
use std::{sync::atomic::AtomicUsize, time::Duration}; use std::{sync::atomic::AtomicUsize, time::Duration};
use tokio::process::Command; use tokio::process::Command;
@@ -326,21 +325,7 @@ impl AppBundle {
// logo.png // logo.png
// ``` // ```
Platform::Web => { Platform::Web => {
// Run wasm-bindgen and drop its output into the assets folder under "dioxus" self.bundle_web().await?;
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()?,
)?;
} }
// this will require some extra oomf to get the multi architecture builds... // 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()) { if keep_bundled_output_paths.contains(canon_path.as_path()) {
return Ok(()); return Ok(());
} }
// Otherwise, if it is a directory, we need to walk it and remove child files // Otherwise, if it is a directory, we need to walk it and remove child files
if path.is_dir() { if path.is_dir() {
for entry in std::fs::read_dir(path)?.flatten() { for entry in std::fs::read_dir(path)?.flatten() {
@@ -427,11 +413,16 @@ impl AppBundle {
// If it is a file, remove it // If it is a file, remove it
tokio::fs::remove_file(path).await?; tokio::fs::remove_file(path).await?;
} }
Ok(()) Ok(())
}) })
} }
tracing::debug!("Removing old assets"); 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?; 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 // 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 { for (asset, bundled) in &self.app.assets.assets {
let from = asset.clone(); let from = asset.clone();
let to = asset_dir.join(bundled.bundled_path()); 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())); 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 // Parallel Copy over the assets and keep track of progress with an atomic counter
let progress = self.build.progress.clone(); 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 // Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
assets_to_transfer assets_to_transfer
.par_iter() .par_iter()
.try_for_each(|(from, to, options)| { .try_for_each(|(from, to, options)| {
let processing = started_processing.fetch_add(1, Ordering::SeqCst); 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); let res = process_file_to(options, from, to);
if let Err(err) = res.as_ref() { if let Err(err) = res.as_ref() {
@@ -486,8 +490,8 @@ impl AppBundle {
.await .await
.map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??; .map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??;
// Remove the wasm bindgen output directory if it exists // // Remove the wasm bindgen output directory if it exists
_ = std::fs::remove_dir_all(self.build.wasm_bindgen_out_dir()); // _ = 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 // Write the version file so we know what version of the optimizer we used
std::fs::write( std::fs::write(
@@ -575,6 +579,7 @@ impl AppBundle {
.krate .krate
.should_pre_compress_web_assets(self.build.build.release); .should_pre_compress_web_assets(self.build.build.release);
self.build.status_compressing_assets();
let asset_dir = self.build.asset_dir(); let asset_dir = self.build.asset_dir();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
crate::fastfs::pre_compress_folder(&asset_dir, pre_compress) crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
@@ -613,114 +618,195 @@ impl AppBundle {
None None
} }
pub(crate) async fn run_wasm_bindgen(&mut self, input_path: &Path) -> anyhow::Result<()> { /// Bundle the web app
tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen"); /// - 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(); // Locate the output of the build files and the bindgen output
// Make sure the bindgen output directory exists // We'll fill these in a second if they don't already exist
let bindgen_outdir = self.build.wasm_bindgen_out_dir(); let bindgen_outdir = self.build.wasm_bindgen_out_dir();
std::fs::create_dir_all(&bindgen_outdir)?; let prebindgen = self.app.exe.clone();
let post_bindgen_wasm = self.build.wasm_bindgen_wasm_output_file();
let name = self.build.krate.executable_name().to_string(); let should_bundle_split = self.build.build.experimental_wasm_split;
let keep_debug = let rustc_exe = self.app.exe.with_extension("wasm");
// 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 bindgen_version = self let bindgen_version = self
.build .build
.krate .krate
.wasm_bindgen_version() .wasm_bindgen_version()
.expect("this should have been checked by tool verification"); .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) WasmBindgen::new(&bindgen_version)
.input_path(&input_path) .input_path(&rustc_exe)
.target("web") .target("web")
.debug(keep_debug) .debug(keep_debug)
.demangle(keep_debug) .demangle(demangle)
.keep_debug(keep_debug) .keep_debug(keep_debug)
.remove_name_section(!keep_debug) .keep_lld_sections(true)
.remove_producers_section(!keep_debug) .out_name(self.build.krate.executable_name())
.out_name(&name)
.out_dir(&bindgen_outdir) .out_dir(&bindgen_outdir)
.remove_name_section(!will_wasm_opt)
.remove_producers_section(!will_wasm_opt)
.run() .run()
.await .await
.context("Failed to generate wasm-bindgen bindings")?; .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()); 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();
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());
} }
#[allow(unused)] // Load the contents of these binaries since we need both of them
pub(crate) fn run_wasm_opt(&self, bindgen_outdir: &std::path::Path) -> Result<()> { // We're going to use the default makeLoad glue from wasm-split
if !self.build.build.release { let original = std::fs::read(&prebindgen)?;
return Ok(()); let bindgened = std::fs::read(&post_bindgen_wasm)?;
}; let mut glue = wasm_split_cli::MAKE_LOAD_JS.to_string();
// 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")?;
// 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(),
)?;
}
// 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(); self.build.status_optimizing_wasm();
wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?;
#[cfg(feature = "optimizations")]
{
use crate::config::WasmOptLevel;
tracing::info!(dx_src = ?TraceSrc::Build, "Running optimization with wasm-opt...");
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)))?;
let new_size = wasm_file.metadata()?.len(); // Make sure to register the main wasm file with the asset system
tracing::debug!( self.app
dx_src = ?TraceSrc::Build, .assets
"wasm-opt reduced WASM size from {} to {} ({:2}%)", .register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?;
old_size,
new_size, // Register the main.js with the asset system so it bundles in the snippets and optimizes
(new_size as f64 - old_size as f64) / old_size as f64 * 100.0 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(()) 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) { pub(crate) fn status_start_bundle(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress { _ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::Bundling {}, 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 { pub(crate) fn is_server(&self) -> bool {
self.build.platform() == Platform::Server self.build.platform() == Platform::Server
} }
+9 -1
View File
@@ -340,6 +340,14 @@ impl BuildRequest {
cargo_args.push(self.krate.executable_name().to_string()); 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); tracing::debug!(dx_src = ?TraceSrc::Build, "cargo args: {:?}", cargo_args);
cargo_args cargo_args
@@ -658,7 +666,7 @@ impl BuildRequest {
/// Get the path to the wasm bindgen temporary output folder /// Get the path to the wasm bindgen temporary output folder
pub fn wasm_bindgen_out_dir(&self) -> PathBuf { 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 /// Get the path to the wasm bindgen javascript output file
+5 -1
View File
@@ -143,7 +143,11 @@ impl AppBundle {
r#" <script> r#" <script>
// We can't use a module script here because we need to start the script immediately when streaming // We can't use a module script here because we need to start the script immediately when streaming
import("/{base_path}/{js_path}").then( import("/{base_path}/{js_path}").then(
({ default: init }) => { ({ 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) => { init("/{base_path}/{wasm_path}").then((wasm) => {
if (wasm.__wbindgen_start == undefined) { if (wasm.__wbindgen_start == undefined) {
wasm.main(); wasm.main();
+4
View File
@@ -51,6 +51,10 @@ pub(crate) struct BuildArgs {
#[clap(long, default_value_t = true)] #[clap(long, default_value_t = true)]
pub(crate) inject_loading_scripts: bool, 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] /// 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 /// 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, pub(crate) https: WebHttpsConfig,
/// Whether to enable pre-compression of assets and wasm during a web build in release mode /// 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, pub(crate) pre_compress: bool,
/// The wasm-opt configuration /// The wasm-opt configuration
@@ -30,7 +30,7 @@ pub(crate) struct WebConfig {
impl Default for WebConfig { impl Default for WebConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
pre_compress: true_bool(), pre_compress: false_bool(),
app: Default::default(), app: Default::default(),
https: Default::default(), https: Default::default(),
wasm_opt: Default::default(), wasm_opt: Default::default(),
@@ -58,13 +58,18 @@ pub(crate) struct WasmOptConfig {
/// Keep debug symbols in the wasm file /// Keep debug symbols in the wasm file
#[serde(default = "false_bool")] #[serde(default = "false_bool")]
pub(crate) debug: 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)] #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
pub(crate) enum WasmOptLevel { pub(crate) enum WasmOptLevel {
/// Optimize aggressively for size /// Optimize aggressively for size
#[serde(rename = "z")] #[serde(rename = "z")]
#[default]
Z, Z,
/// Optimize for size /// Optimize for size
#[serde(rename = "s")] #[serde(rename = "s")]
@@ -83,7 +88,6 @@ pub(crate) enum WasmOptLevel {
Three, Three,
/// Optimize aggressively for speed /// Optimize aggressively for speed
#[serde(rename = "4")] #[serde(rename = "4")]
#[default]
Four, 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(); let entry_path = entry.path();
if entry_path.is_file() { if entry_path.is_file() {
if pre_compress { if pre_compress {
tracing::info!("Pre-compressing file {}", entry_path.display());
if let Err(err) = pre_compress_file(entry_path) { if let Err(err) = pre_compress_file(entry_path) {
tracing::error!("Failed to pre-compress file {entry_path:?}: {err}"); tracing::error!("Failed to pre-compress file {entry_path:?}: {err}");
} }
+13 -4
View File
@@ -68,10 +68,12 @@ impl TraceController {
let filter = if env::var(LOG_ENV).is_ok() { let filter = if env::var(LOG_ENV).is_ok() {
EnvFilter::from_env(LOG_ENV) EnvFilter::from_env(LOG_ENV)
} else if matches!(args.action, Commands::Serve(_)) { } 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 { } else {
EnvFilter::new(format!( 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 { our_level = if args.verbosity.verbose {
"debug" "debug"
} else { } else {
@@ -131,10 +133,13 @@ impl TraceController {
} }
/// Get a handle to the trace controller. /// Get a handle to the trace controller.
pub fn redirect() -> Self { pub fn redirect(interactive: bool) -> Self {
let (tui_tx, tui_rx) = unbounded(); let (tui_tx, tui_rx) = unbounded();
if interactive {
TUI_ACTIVE.store(true, Ordering::Relaxed); TUI_ACTIVE.store(true, Ordering::Relaxed);
TUI_TX.set(tui_tx.clone()).unwrap(); TUI_TX.set(tui_tx.clone()).unwrap();
}
Self { tui_rx } Self { tui_rx }
} }
@@ -142,7 +147,11 @@ impl TraceController {
/// Wait for the internal logger to send a message /// Wait for the internal logger to send a message
pub(crate) async fn wait(&mut self) -> ServeUpdate { pub(crate) async fn wait(&mut self) -> ServeUpdate {
use futures_util::StreamExt; 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 } ServeUpdate::TracingLog { log }
} }
} }
+1
View File
@@ -19,6 +19,7 @@ mod rustc;
mod serve; mod serve;
mod settings; mod settings;
mod wasm_bindgen; mod wasm_bindgen;
mod wasm_opt;
pub(crate) use build::*; pub(crate) use build::*;
pub(crate) use cli::*; 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}"); 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 // adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,); 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. /// to a dynamic one on the fly.
pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> { pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
// Redirect all logging the cli logger // 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 // 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?; let krate = args.load_krate().await?;
+2
View File
@@ -533,6 +533,8 @@ impl Output {
lines.push(krate.as_str().dark_gray()) lines.push(krate.as_str().dark_gray())
} }
BuildStage::OptimizingWasm {} => lines.push("Optimizing wasm".yellow()), 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::PrerenderingRoutes {} => lines.push("Prerendering static routes".yellow()),
BuildStage::RunningBindgen {} => lines.push("Running wasm-bindgen".yellow()), BuildStage::RunningBindgen {} => lines.push("Running wasm-bindgen".yellow()),
BuildStage::RunningGradle {} => lines.push("Running gradle assemble".yellow()), BuildStage::RunningGradle {} => lines.push("Running gradle assemble".yellow()),
+26 -11
View File
@@ -18,10 +18,11 @@ pub(crate) struct WasmBindgen {
demangle: bool, demangle: bool,
remove_name_section: bool, remove_name_section: bool,
remove_producers_section: bool, remove_producers_section: bool,
keep_lld_exports: bool,
} }
impl WasmBindgen { impl WasmBindgen {
pub fn new(version: &str) -> Self { pub(crate) fn new(version: &str) -> Self {
Self { Self {
version: version.to_string(), version: version.to_string(),
input_path: PathBuf::new(), input_path: PathBuf::new(),
@@ -33,65 +34,73 @@ impl WasmBindgen {
demangle: true, demangle: true,
remove_name_section: false, remove_name_section: false,
remove_producers_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 { Self {
input_path: input_path.to_path_buf(), input_path: input_path.to_path_buf(),
..self ..self
} }
} }
pub fn out_dir(self, out_dir: &Path) -> Self { pub(crate) fn out_dir(self, out_dir: &Path) -> Self {
Self { Self {
out_dir: out_dir.to_path_buf(), out_dir: out_dir.to_path_buf(),
..self ..self
} }
} }
pub fn out_name(self, out_name: &str) -> Self { pub(crate) fn out_name(self, out_name: &str) -> Self {
Self { Self {
out_name: out_name.to_string(), out_name: out_name.to_string(),
..self ..self
} }
} }
pub fn target(self, target: &str) -> Self { pub(crate) fn target(self, target: &str) -> Self {
Self { Self {
target: target.to_string(), target: target.to_string(),
..self ..self
} }
} }
pub fn debug(self, debug: bool) -> Self { pub(crate) fn debug(self, debug: bool) -> Self {
Self { debug, ..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 } Self { keep_debug, ..self }
} }
pub fn demangle(self, demangle: bool) -> Self { pub(crate) fn demangle(self, demangle: bool) -> Self {
Self { demangle, ..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 { Self {
remove_name_section, remove_name_section,
..self ..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 { Self {
remove_producers_section, remove_producers_section,
..self ..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 /// 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 binary = self.get_binary_path().await?;
let mut args = Vec::new(); let mut args = Vec::new();
@@ -121,6 +130,10 @@ impl WasmBindgen {
args.push("--remove-producers-section"); args.push("--remove-producers-section");
} }
if self.keep_lld_exports {
args.push("--keep-lld-exports");
}
// Out name // Out name
args.push("--out-name"); args.push("--out-name");
args.push(&self.out_name); args.push(&self.out_name);
@@ -144,6 +157,8 @@ impl WasmBindgen {
.expect("input_path should be valid utf8"); .expect("input_path should be valid utf8");
args.push(input_path); args.push(input_path);
tracing::debug!("wasm-bindgen args: {:#?}", args);
// Run bindgen // Run bindgen
Command::new(binary) Command::new(binary)
.args(args) .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(())
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "dioxus-config-macros"
edition = "2021"
version.workspace = true
authors = ["Dioxus Labs"]
description = "Macros used internally by codegen"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["web", "desktop", "mobile", "gui", "wasm"]
rust-version = "1.79.0"
[dependencies]
[features]
default = []
wasm-split = []
+15
View File
@@ -0,0 +1,15 @@
# Dioxus Config Macros
These macros are used internally by codegen and are not intended for general use.
Dioxus will export its feature flags into this crate, allowing downstream codegen to use them under the "dioxus" namespace.
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/dioxuslabs/dioxus/blob/main/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
+39
View File
@@ -0,0 +1,39 @@
/// A macro for deciding whether or not to split the wasm bundle.
/// Used by the internal router-macro code. The contents here are considered to be semver exempt.
///
/// Only on wasm with the wasm-split feature will we prefer the `maybe_wasm_split` variant that emits
/// the "lefthand" tokens. Otherwise, we emit the non-wasm_split tokens
#[doc(hidden)]
#[cfg(all(feature = "wasm-split", target_arch = "wasm32"))]
#[macro_export]
macro_rules! maybe_wasm_split {
(
if wasm_split {
$left:tt
} else {
$right:tt
}
) => {
$left
};
}
/// A macro for deciding whether or not to split the wasm bundle.
/// Used by the internal router-macro code. The contents here are considered to be semver exempt.
///
/// Only on wasm with the wasm-split feature will we prefer the `maybe_wasm_split` variant that emits
/// the "lefthand" tokens. Otherwise, we emit the non-wasm_split tokens
#[doc(hidden)]
#[cfg(any(not(feature = "wasm-split"), not(target_arch = "wasm32")))]
#[macro_export]
macro_rules! maybe_wasm_split {
(
if wasm_split {
$left:tt
} else {
$right:tt
}
) => {
$right
};
}
+128 -3
View File
@@ -1,18 +1,29 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::*; use syn::*;
pub struct ComponentBody { pub struct ComponentBody {
pub item_fn: ItemFn, pub item_fn: ItemFn,
pub options: ComponentMacroOptions,
} }
impl Parse for ComponentBody { impl Parse for ComponentBody {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
let item_fn: ItemFn = input.parse()?; let item_fn: ItemFn = input.parse()?;
validate_component_fn(&item_fn)?; validate_component_fn(&item_fn)?;
Ok(Self { item_fn }) Ok(Self {
item_fn,
options: ComponentMacroOptions::default(),
})
}
}
impl ComponentBody {
pub fn with_options(mut self, options: ComponentMacroOptions) -> Self {
self.options = options;
self
} }
} }
@@ -100,17 +111,99 @@ impl ComponentBody {
quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident #impl_generics } quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident #impl_generics }
}; };
// Defer to the lazy_body if we're using lazy
let body: TokenStream = if self.options.lazy {
self.lazy_body(
&struct_ident,
generics,
&impl_generics,
fn_output,
where_clause,
&inlined_props_argument,
block,
)
} else {
quote! { #block }
};
// We need a props type to exist even if the inputs are empty with lazy components
let emit_props = if self.options.lazy {
if inputs.is_empty() {
quote! {props: ()}
} else {
quote!(props: #struct_ident #impl_generics)
}
} else {
inlined_props_argument
};
// The extra nest is for the snake case warning to kick back in // The extra nest is for the snake case warning to kick back in
parse_quote! { parse_quote! {
#(#attrs)* #(#attrs)*
#(#props_docs)* #(#props_docs)*
#[allow(non_snake_case)] #[allow(non_snake_case)]
#vis fn #fn_ident #generics (#inlined_props_argument) #fn_output #where_clause { #vis fn #fn_ident #generics (#emit_props) #fn_output #where_clause {
{ {
// In debug mode we can detect if the user is calling the component like a function // In debug mode we can detect if the user is calling the component like a function
dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish); dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
#body
}
}
}
}
/// Generate the body of the lazy component
///
/// This extracts the body into a new component that is wrapped in a lazy loader
#[allow(clippy::too_many_arguments)]
fn lazy_body(
&self,
struct_ident: &Ident,
generics: &Generics,
impl_generics: &TypeGenerics,
fn_output: &ReturnType,
where_clause: &Option<WhereClause>,
inlined_props_argument: &TokenStream,
block: &Block,
) -> TokenStream {
let fn_ident = &self.item_fn.sig.ident;
let inputs = &self.item_fn.sig.inputs;
let lazy_name = format_ident!("Lazy{fn_ident}");
let out_ty = match &self.item_fn.sig.output {
ReturnType::Default => quote! { () },
ReturnType::Type(_, ty) => quote! { #ty },
};
let props_ty = if inputs.is_empty() {
quote! { () }
} else {
quote! { #struct_ident #impl_generics }
};
let anon_props = if inputs.is_empty() {
quote! { props: () }
} else {
quote! { #inlined_props_argument}
};
quote! {
fn #lazy_name #generics (#anon_props) #fn_output #where_clause {
#block #block
} }
dioxus::config_macros::maybe_wasm_split! {
if wasm_split {
{
static __MODULE: wasm_split::LazyLoader<#props_ty, #out_ty> =
wasm_split::lazy_loader!(extern "lazy" fn #lazy_name(props: #props_ty,) -> #out_ty);
use_resource(|| async move { __MODULE.load().await }).suspend()?;
__MODULE.call(props).unwrap()
}
} else {
{
#lazy_name(props)
}
}
} }
} }
} }
@@ -467,3 +560,35 @@ fn allow_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn {
clone clone
} }
#[derive(Default)]
pub struct ComponentMacroOptions {
pub lazy: bool,
}
impl Parse for ComponentMacroOptions {
fn parse(input: ParseStream) -> Result<Self> {
let mut lazy_load = false;
while !input.is_empty() {
let ident = input.parse::<Ident>()?;
let ident_name = ident.to_string();
if ident_name == "lazy" {
lazy_load = true;
} else if ident_name == "no_case_check" {
// we used to have this?
} else {
return Err(Error::new(
ident.span(),
"Unknown option for component macro",
));
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(Self { lazy: lazy_load })
}
}
+2 -1
View File
@@ -2,7 +2,7 @@
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use component::ComponentBody; use component::{ComponentBody, ComponentMacroOptions};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::ToTokens; use quote::ToTokens;
use syn::parse_macro_input; use syn::parse_macro_input;
@@ -36,6 +36,7 @@ pub fn rsx(tokens: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream { pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
parse_macro_input!(input as ComponentBody) parse_macro_input!(input as ComponentBody)
.with_options(parse_macro_input!(_args as ComponentMacroOptions))
.into_token_stream() .into_token_stream()
.into() .into()
} }
+16 -8
View File
@@ -75,21 +75,29 @@ impl<F: ComponentFunction<P, M> + Clone, P: Clone + 'static, M: 'static> AnyProp
} }
fn render(&self) -> Element { fn render(&self) -> Element {
let res = std::panic::catch_unwind(AssertUnwindSafe(move || { fn render_inner(name: &str, res: Result<Element, Box<dyn Any + Send>>) -> Element {
self.render_fn.rebuild(self.props.clone())
}));
match res { match res {
Ok(node) => node, Ok(node) => node,
Err(err) => { Err(err) => {
let component_name = self.name; // on wasm this massively bloats binary sizes and we can't even capture the panic
tracing::error!("Panic while rendering component `{component_name}`: {err:?}"); // so do nothing
let panic = CapturedPanic { error: err }; #[cfg(not(target_arch = "wasm32"))]
Element::Err(panic.into()) {
tracing::error!("Panic while rendering component `{name}`: {err:?}");
}
Element::Err(CapturedPanic { error: err }.into())
} }
} }
} }
render_inner(
self.name,
std::panic::catch_unwind(AssertUnwindSafe(move || {
self.render_fn.rebuild(self.props.clone())
})),
)
}
fn duplicate(&self) -> BoxedAnyProps { fn duplicate(&self) -> BoxedAnyProps {
Box::new(Self { Box::new(Self {
render_fn: self.render_fn.clone(), render_fn: self.render_fn.clone(),
+10
View File
@@ -450,6 +450,16 @@ impl Scope {
hooks.push(Box::new(initializer())); hooks.push(Box::new(initializer()));
} }
self.use_hook_inner::<State>(hooks, cur_hook)
}
// The interior version that gets monoorphized by the `State` type but not the `initializer` type.
// This helps trim down binary sizes
fn use_hook_inner<State: Clone + 'static>(
&self,
hooks: std::cell::RefMut<Vec<Box<dyn std::any::Any>>>,
cur_hook: usize,
) -> State {
hooks hooks
.get(cur_hook) .get(cur_hook)
.and_then(|inn| { .and_then(|inn| {
+11 -1
View File
@@ -162,6 +162,16 @@ impl Runtime {
scope: ScopeId, scope: ScopeId,
task: impl Future<Output = ()> + 'static, task: impl Future<Output = ()> + 'static,
ty: TaskType, ty: TaskType,
) -> Task {
self.spawn_task_of_type_inner(scope, Box::pin(task), ty)
}
// a non-momorphic version of spawn_task_of_type, helps with binari sizes
fn spawn_task_of_type_inner(
&self,
scope: ScopeId,
pinned_task: Pin<Box<dyn Future<Output = ()>>>,
ty: TaskType,
) -> Task { ) -> Task {
// Insert the task, temporarily holding a borrow on the tasks map // Insert the task, temporarily holding a borrow on the tasks map
let (task, task_id) = { let (task, task_id) = {
@@ -176,7 +186,7 @@ impl Runtime {
scope, scope,
active: Cell::new(true), active: Cell::new(true),
parent: self.current_task(), parent: self.current_task(),
task: RefCell::new(Box::pin(task)), task: RefCell::new(pinned_task),
waker: futures_util::task::waker(Arc::new(LocalTaskHandle { waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
id: task_id.id, id: task_id.id,
tx: self.sender.clone(), tx: self.sender.clone(),
+3
View File
@@ -12,6 +12,7 @@ rust-version = "1.79.0"
[dependencies] [dependencies]
dioxus-core = { workspace = true } dioxus-core = { workspace = true }
dioxus-config-macros = { workspace = true }
dioxus-html = { workspace = true, default-features = false, optional = true } dioxus-html = { workspace = true, default-features = false, optional = true }
dioxus-document = { workspace = true, optional = true } dioxus-document = { workspace = true, optional = true }
dioxus-history = { workspace = true, optional = true } dioxus-history = { workspace = true, optional = true }
@@ -29,6 +30,7 @@ dioxus-ssr = { workspace = true, optional = true }
manganis = { workspace = true, features = ["dioxus"], optional = true } manganis = { workspace = true, features = ["dioxus"], optional = true }
dioxus-logger = { workspace = true, optional = true } dioxus-logger = { workspace = true, optional = true }
warnings = { workspace = true, optional = true } warnings = { workspace = true, optional = true }
wasm-split = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
dioxus-cli-config = { workspace = true, optional = true } dioxus-cli-config = { workspace = true, optional = true }
@@ -51,6 +53,7 @@ document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"]
logger = ["dep:dioxus-logger"] logger = ["dep:dioxus-logger"]
cli-config = ["dep:dioxus-cli-config"] cli-config = ["dep:dioxus-cli-config"]
warnings = ["dep:warnings"] warnings = ["dep:warnings"]
wasm-split = ["dep:wasm-split", "dioxus-config-macros/wasm-split"] # note: to turn on the router splitter, you need to manually enable wasm-split on the router
launch = ["dep:dioxus-config-macro"] launch = ["dep:dioxus-config-macro"]
router = ["dep:dioxus-router"] router = ["dep:dioxus-router"]
+10
View File
@@ -74,6 +74,10 @@ pub use dioxus_logger as logger;
#[cfg_attr(docsrs, doc(cfg(feature = "cli-config")))] #[cfg_attr(docsrs, doc(cfg(feature = "cli-config")))]
pub use dioxus_cli_config as cli_config; pub use dioxus_cli_config as cli_config;
#[cfg(feature = "wasm-split")]
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-split")))]
pub use wasm_split;
pub mod prelude { pub mod prelude {
#[cfg(feature = "document")] #[cfg(feature = "document")]
#[cfg_attr(docsrs, doc(cfg(feature = "document")))] #[cfg_attr(docsrs, doc(cfg(feature = "document")))]
@@ -138,6 +142,10 @@ pub mod prelude {
#[cfg(feature = "asset")] #[cfg(feature = "asset")]
#[cfg_attr(docsrs, doc(cfg(feature = "asset")))] #[cfg_attr(docsrs, doc(cfg(feature = "asset")))]
pub use manganis::{self, *}; pub use manganis::{self, *};
#[cfg(feature = "wasm-split")]
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-split")))]
pub use wasm_split;
} }
#[cfg(feature = "web")] #[cfg(feature = "web")]
@@ -171,3 +179,5 @@ pub use dioxus_ssr as ssr;
#[cfg(feature = "warnings")] #[cfg(feature = "warnings")]
#[cfg_attr(docsrs, doc(cfg(feature = "warnings")))] #[cfg_attr(docsrs, doc(cfg(feature = "warnings")))]
pub use warnings; pub use warnings;
pub use dioxus_config_macros as config_macros;
+2
View File
@@ -56,6 +56,7 @@ pub enum BuildStage {
krate: String, krate: String,
}, },
RunningBindgen, RunningBindgen,
SplittingBundle,
OptimizingWasm, OptimizingWasm,
PrerenderingRoutes, PrerenderingRoutes,
CopyingAssets { CopyingAssets {
@@ -69,4 +70,5 @@ pub enum BuildStage {
Failed, Failed,
Aborted, Aborted,
Restarting, Restarting,
CompressingAssets,
} }
+10 -1
View File
@@ -142,11 +142,20 @@ module.exports = defineConfig({
cwd: path.join(process.cwd(), "cli-optimization"), cwd: path.join(process.cwd(), "cli-optimization"),
// Remove the cache folder for the cli-optimization build to force a full cache reset // Remove the cache folder for the cli-optimization build to force a full cache reset
command: command:
'cargo run --package dioxus-cli --release -- serve --addr "127.0.0.1" --port 8989', 'cargo run --package dioxus-cli --release --features optimizations -- serve --addr "127.0.0.1" --port 8989',
port: 8989, port: 8989,
timeout: 50 * 60 * 1000, timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
stdout: "pipe", stdout: "pipe",
}, },
{
cwd: path.join(process.cwd(), "wasm-split-harness"),
command:
'cargo run --package dioxus-cli --release --features optimizations -- serve --bin wasm-split-harness --platform web --addr "127.0.0.1" --port 8001 --experimental-wasm-split --profile wasm-split-release',
port: 8001,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
], ],
}); });
@@ -0,0 +1,21 @@
[package]
name = "wasm-split-harness"
version = "0.1.0"
edition = "2021"
publish = false
authors = ["Jonathan Kelley"]
[dependencies]
dioxus = { workspace = true, features = ["web", "router", "wasm-split"] }
dioxus-router = { workspace = true, features = ["wasm-split"] }
anyhow = { workspace = true }
async-compression = { workspace = true, features = ["futures-io", "gzip", "brotli"] }
futures = { workspace = true }
js-sys = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
web-sys = { workspace = true, features = ["Document", "Window", "HtmlElement", "Text", "DomRectReadOnly", "console"] }
once_cell = { workspace = true }
getrandom = { workspace = true, features = ["js"] }
reqwest = { workspace = true, features = ["json"] }
@@ -0,0 +1 @@
harness/
@@ -0,0 +1,13 @@
<html>
<head>
<meta charset="utf-8">
<title>Hello wasm-bindgen</title>
</head>
<body>
<div id="main"></div>
</body>
<script type="module">
import init from "/harness/split/main.js";
init();
</script>
</html>
+14
View File
@@ -0,0 +1,14 @@
cargo build --bin wasm-split-cli
CLI=./target/debug/wasm-split-cli
rm -rf docsite/chunks
# Run the wasm-split-cli on the with_body.wasm file
${CLI} split docsite/input.wasm docsite/bindgen/main_bg.wasm docsite/chunks
# copy the contents of the wasm_bindgen folder to the docsite folder
mv docsite/chunks/main.wasm docsite/chunks/main_bg.wasm # rename the main wasm file
cp -r docsite/bindgen/snippets docsite/chunks/snippets
cp docsite/bindgen/main.js docsite/chunks/main.js
python3 -m http.server 8080 --directory docsite
+82
View File
@@ -0,0 +1,82 @@
# This file is a simple shell script that runs the bundle split process manually without the CLI involved
# it's not necessarily meant to work on your machine (sorry!)
#
# To hack on harness you need the `wasm-tools` CLI installed
# `cargo binstall wasm-tools`
#
# This script is also sensitive to where it's run from, so you *need* to be in the harness folder (running as `./run.sh`)
TARGET_DIR=../../../target
# build the harness
cargo rustc --package wasm-split-harness --target wasm32-unknown-unknown --profile wasm-split-release -- -Clink-args=--emit-relocs
# for a much smaller compile, you can crank up the flags. However, dioxus relies heavily on location detail, so we can't disable that
#
# -Zlocation-detail=none - we could compile with location detail off but if breaks our signals...
#
# cargo +nightly rustc \
# -Z build-std=std,panic_abort \
# -Z build-std-features="optimize_for_size" \
# -Z build-std-features=panic_immediate_abort \
# --target wasm32-unknown-unknown \
# --no-default-features \
# --profile wasm-split-release \
# -- -Clink-args=--emit-relocs
# Build the wasm-split-cli. We are going to call it directly since it's so noisy to build it multiple times
cargo build --package wasm-split-cli --bin wasm-split-cli
CLI=$TARGET_DIR/debug/wasm-split-cli
# clear the workdir and assemble the new structure
rm -rf data/harness
mkdir -p data/harness/split
mkdir -p data/harness/split_not
# copy the output wasm file to the harness dir
cp $TARGET_DIR/wasm32-unknown-unknown/wasm-split-release/wasm-split-harness.wasm data/harness/input.wasm
# Run wasm-bindgen on this module, without splitting it
wasm-bindgen data/harness/input.wasm --out-dir data/harness/split_not --target web --out-name main --no-demangle --no-typescript --keep-lld-exports --keep-debug
# Run the wasm-split-cli on the with_body.wasm file
${CLI} split data/harness/input.wasm data/harness/split_not/main_bg.wasm data/harness/chunks
# copy over the chunks
paths=$(ls data/harness/chunks/ | grep "\.wasm")
for path in $paths
do
path_without_ext=${path%.*}
wasm-opt -Oz data/harness/chunks/$path -o data/harness/split/$path --enable-reference-types --memory-packing --debuginfo
# remove stuff like manganis, etc
wasm-tools strip data/harness/split/$path -o data/harness/split/$path
# if you don't want names (making it harder to debug the outputs) use `--all`
# wasm-tools strip data/harness/split/$path -o data/harness/split/$path --all
done
# rename the main chunk
mv data/harness/split/main.wasm data/harness/split/main_bg.wasm
cp data/harness/split_not/main.js data/harness/split/main.js
cp -r data/harness/split_not/snippets data/harness/split/snippets
cp data/harness/chunks/__wasm_split.js data/harness/split/__wasm_split.js
wasm-opt -Oz data/harness/split_not/main_bg.wasm -o data/harness/split_not/main_bg_opt.wasm --enable-reference-types --memory-packing --debuginfo
# Run wasm-strip to strip out the debug symbols
wasm-tools strip data/harness/split_not/main_bg_opt.wasm -o data/harness/split_not/main_bg_opt.wasm
# if you don't want names (making it harder to debug the outputs) use `--all`
# wasm-tools strip data/harness/split/$path -o strip data/harness/split_not/main_bg_opt.wasm --all
echo "===========================================================================\n"
ls -l data/harness/split_not/main_bg_opt.wasm | awk '{ printf("%07d -> ", $5);print $9}'
echo ""
ls -l data/harness/split | grep "\.wasm" | awk '{ printf("%07d -> ", $5);print $9}'
echo "\n==========================================================================="
# hope you have python3 installed :)
python3 -m http.server 9876 --directory data
@@ -0,0 +1,296 @@
//! This file is a fuzz-test for the wasm-split engine to ensure that it works as expected.
//! The docsite is a better target for this, but we try to boil down the complexity into this small
//! test file.
#![allow(non_snake_case)]
use dioxus::prelude::*;
use futures::AsyncReadExt;
use js_sys::Date;
use std::pin::Pin;
use wasm_bindgen::prelude::*;
use wasm_split::lazy_loader;
fn main() {
dioxus::launch(|| {
rsx! {
Router::<Route> {}
}
});
}
#[derive(Routable, PartialEq, Eq, Debug, Clone)]
enum Route {
#[layout(Nav)]
#[route("/")]
Home,
#[route("/child")]
ChildSplit,
}
fn Nav() -> Element {
rsx! {
div {
Link { id: "link-home", to: Route::Home, "Home" }
Link { id: "link-child", to: Route::ChildSplit, "Child" }
Outlet::<Route> {}
}
}
}
pub(crate) static GLOBAL_COUNTER: GlobalSignal<usize> = Signal::global(|| 0);
fn Home() -> Element {
let mut count = use_signal(|| 1);
let mut res = use_signal(|| "hello".to_string());
rsx! {
h1 { "Hello bundle split 456" }
h3 { id: "counter-display", "Count: {count}" }
h3 { id: "global-counter", "Global Counter: {GLOBAL_COUNTER}" }
button {
id: "increment-counter",
onclick: move |_| count += 1,
"Click me"
}
button {
id: "increment-counter-global",
onclick: move |_| *GLOBAL_COUNTER.write() += 1,
"Click me"
}
button {
id: "add-body-text",
onclick: move |_| add_body_text(),
"Add body text"
}
button {
id: "add-body-element",
onclick: move |_| async move {
add_body_element().await;
count += 1;
},
"Add body element"
}
button {
id: "gzip-it",
onclick: move |_| async move {
gzip_it().await;
},
"GZIP it"
}
button {
id: "brotli-it",
onclick: move |_| async move {
brotli_it(&[0u8; 10]).await;
},
"Brotli It"
}
button {
id: "make-request",
onclick: move |_| async move {
let res_ = make_request().await.unwrap();
res.set(res_);
},
"Make Request!"
}
button {
id: "make-local-request",
onclick: move |_| async move {
let client = reqwest::Client::new();
let response = client
.get("https://dog.ceo/api/breeds/image/random")
.send()
.await?;
let body = response.text().await?;
*res.write() = body;
Ok(())
},
"local request"
}
LazyComponent {
abc: 0
}
div { "Response: {res}" }
div { id: "output-box" }
}
}
#[wasm_split::wasm_split(one)]
async fn add_body_text() {
let window = web_sys::window().unwrap_throw();
let document = window.document().unwrap_throw();
let output = document.create_text_node("Rendered!");
let output_box = document.get_element_by_id("output-box").unwrap_throw();
output_box.append_child(&output).unwrap_throw();
*GLOBAL_COUNTER.write() += 1;
}
#[wasm_split::wasm_split(two)]
async fn add_body_element() {
let window = web_sys::window().unwrap_throw();
let document = window.document().unwrap_throw();
let output = document.create_element("div").unwrap_throw();
output.set_text_content(Some("Some inner div"));
let output_box = document.get_element_by_id("output-box").unwrap_throw();
output_box.append_child(&output).unwrap_throw();
dioxus::prelude::queue_effect(move || {
web_sys::console::log_1(&"add body async internal!".into());
*GLOBAL_COUNTER.write() += 2;
});
}
#[wasm_split::wasm_split(four)]
async fn gzip_it() {
static DATA: &[u8] = &[0u8; 10];
let reader = Box::pin(futures::io::BufReader::new(DATA));
let reader: Pin<Box<dyn futures::io::AsyncBufRead>> = reader;
dioxus::prelude::spawn(async move {
let mut fut = Box::pin(async_compression::futures::bufread::GzipDecoder::new(
reader,
));
if fut.read_to_end(&mut Vec::new()).await.is_err() {
web_sys::console::log_1(&"error reading gzip".into());
}
*GLOBAL_COUNTER.write() += 3;
let res: Result<String, anyhow::Error> = Box::pin(async move {
let client = reqwest::Client::new();
let response = client
.get("https://dog.ceo/api/breeds/image/random")
.send()
.await?;
let body = response.text().await?;
Ok(body)
})
.await;
if res.is_err() {
web_sys::console::log_1(&"error making request".into());
}
});
}
#[wasm_split::wasm_split(three)]
async fn brotli_it(data: &'static [u8]) {
let reader = Box::pin(futures::io::BufReader::new(data));
let reader: Pin<Box<dyn futures::io::AsyncBufRead>> = reader;
dioxus::prelude::spawn(async move {
let mut fut = Box::pin(async_compression::futures::bufread::BrotliDecoder::new(
reader,
));
if fut.read_to_end(&mut Vec::new()).await.is_err() {
web_sys::console::log_1(&"error reading brotli".into());
}
*GLOBAL_COUNTER.write() += 4;
});
}
#[wasm_split::wasm_split(eleven)]
async fn make_request() -> Result<String, anyhow::Error> {
let client = reqwest::Client::new();
let response = client
.get("https://dog.ceo/api/breeds/image/random")
.send()
.await?;
let body = response.text().await?;
Ok(body)
}
#[component(lazy)]
fn LazyComponent(abc: i32) -> Element {
rsx! {
div {
"This is a lazy component! {abc}"
}
}
}
fn ChildSplit() -> Element {
pub(crate) static DATE: GlobalSignal<Date> = Signal::global(Date::new_0);
static LOADER: wasm_split::LazyLoader<(), Element> =
lazy_loader!(extern "five" fn InnerChild(props: ()) -> Element);
fn InnerChild(_props: ()) -> Element {
static LOADER2: wasm_split::LazyLoader<Signal<String>, Element> =
lazy_loader!(extern "fortytwo" fn InnerChild3(props: Signal<String>) -> Element);
fn InnerChild3(count: Signal<String>) -> Element {
pub(crate) static ICONCHECK: Component<()> = |_| {
rsx! {
svg {
class: "octicon octicon-check js-clipboard-check-icon d-inline-block d-none",
fill: "rgb(26, 127, 55)",
height: "24",
version: "1.1",
"aria_hidden": "true",
width: "24",
view_box: "0 0 16 16",
"data_view_component": "true",
path {
d: "M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z",
fill_rule: "evenodd",
}
}
button {
onclick: move |_| {
*DATE.write() = Date::new_0();
},
"Update Date"
}
}
};
let now = DATE.read().clone();
rsx! {
h1 { "Some other child" }
h3 { "Global Counter: {GLOBAL_COUNTER}" }
h3 { "Date: {now.to_date_string()}" }
h3 { "count: {count}" }
ICONCHECK {}
}
}
#[wasm_bindgen(module = "/src/stars.js")]
extern "C" {
pub(crate) fn get_stars(name: String) -> Option<usize>;
pub(crate) fn set_stars(name: String, stars: usize);
}
let num = get_stars("stars".to_string()).unwrap_or(0);
use_resource(|| async move { LOADER2.load().await }).suspend()?;
let mut count = use_signal(|| "hello".to_string());
let fp = LOADER2.call(count).unwrap();
rsx! {
h1 { "Some huge child?" }
p { "Stars: {num}" }
button {
onclick: move |_| {
set_stars("stars".to_string(), num + 1);
dioxus::prelude::needs_update();
},
"Add Star"
}
{fp}
h3 { id: "nested-child-count", "Count: {count}" }
button {
id: "nested-child-add-world",
onclick: move |_| {
*count.write() += " world";
},
"Add World"
}
}
}
use_resource(|| async move { LOADER.load().await }).suspend()?;
LOADER.call(()).unwrap()
}
@@ -0,0 +1,29 @@
// Handle caching of github stars
// This file is part of the fuzz test to prove we can handle linked js files
// Two days
const STAR_EXPIRE_TIME = 172800000;
export function get_stars(name) {
let item = localStorage.getItem(name);
let data = JSON.parse(item);
if (!data) {
return null;
}
if (data.expires <= Date.now()) {
localStorage.removeItem(name);
return null;
}
return data.stars;
}
export function set_stars(name, value) {
let expires = Date.now() + STAR_EXPIRE_TIME;
let data = { stars: value, expires };
let converted = JSON.stringify(data);
localStorage.setItem(name, converted);
}
@@ -0,0 +1,49 @@
// @ts-check
const { test, expect } = require("@playwright/test");
test("wasm-split page is functional", async ({ page }) => {
// Wait for the dev server to load
await page.goto("http://localhost:8001");
// Make sure the local button works - no broken wasm
const counter = page.locator("#counter-display");
await expect(counter).toContainText("Count: 1");
await page.locator("#increment-counter").click();
await expect(counter).toContainText("Count: 2");
// Make sure the global button works - no broken wasm
const counterGlobal = page.locator("#global-counter");
await expect(counterGlobal).toContainText("Global Counter: 0");
await page.locator("#increment-counter-global").click();
await expect(counterGlobal).toContainText("Global Counter: 1");
// Fire one of the wasm modules to load. Should update the counter and add some text
const addBodyTextButton = page.locator("#add-body-text");
await addBodyTextButton.click();
await expect(counterGlobal).toContainText("Global Counter: 2");
const outputBox = page.locator("#output-box");
await expect(outputBox).toContainText("Rendered!");
// The other wasm module
const addBodyElementButton = page.locator("#add-body-element");
await addBodyElementButton.click();
await expect(counterGlobal).toContainText("Global Counter: 4");
await expect(outputBox).toContainText("Some inner div");
// Load the gzip and brotli modules
const gzipButton = page.locator("#gzip-it");
await gzipButton.click();
await expect(counterGlobal).toContainText("Global Counter: 7");
const brotliButton = page.locator("#brotli-it");
await brotliButton.click();
await expect(counterGlobal).toContainText("Global Counter: 11");
// Ignore the requests in CI
// Load the other router module
const childRouteButton = page.locator("#link-child");
await childRouteButton.click();
const nestedChildCounter = page.locator("#nested-child-count");
await expect(nestedChildCounter).toContainText("Count: hello");
await page.locator("#nested-child-add-world").click();
await expect(nestedChildCounter).toContainText("Count: hello world");
});
+4 -1
View File
@@ -19,13 +19,16 @@ syn = { workspace = true, features = ["extra-traits", "full"] }
quote = { workspace = true } quote = { workspace = true }
proc-macro2 = { workspace = true } proc-macro2 = { workspace = true }
slab = { workspace = true } slab = { workspace = true }
base16 = { workspace = true }
digest = { workspace = true }
sha2 = { workspace = true }
[dev-dependencies] [dev-dependencies]
dioxus = { workspace = true, features = ["router"] } dioxus = { workspace = true, features = ["router"] }
[features] [features]
default = [] default = []
web = [] web = [] #todo(jon) remove this before releasing 0.7!
[package.metadata.docs.rs] [package.metadata.docs.rs]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
+1 -74
View File
@@ -475,82 +475,9 @@ impl RouteEnum {
site_map, site_map,
}; };
// If we're on the web, only the URL history is preserved between navigation. We need to warn the user that the segment is not present in the URL.
if cfg!(feature = "web") {
for variant in &data.variants {
for field in &variant.fields {
if !myself.field_present_in_url(field.ident.as_ref().unwrap()) {
return Err(syn::Error::new_spanned(
field.ident.as_ref().unwrap(),
format!("The `{}` field must be present in the url for the web history. You can include the field in the url by using the `#[route(\"/:{}\")]` attribute on the enum variant.", field.ident.as_ref().unwrap(), field.ident.as_ref().unwrap()),
));
}
}
}
}
Ok(myself) Ok(myself)
} }
fn field_present_in_url(&self, field: &Ident) -> bool {
let mut from_route = false;
for nest in &self.nests {
if nest.dynamic_segments_names().any(|i| &i == field) {
from_route = true
}
}
for route in &self.endpoints {
match route {
RouteEndpoint::Route(route) => match &route.ty {
RouteType::Child(child) => {
if let Some(child) = child.ident.as_ref() {
if child == "child" {
from_route = true
}
}
}
RouteType::Leaf { .. } => {
for segment in &route.segments {
if segment.name().as_ref() == Some(field) {
from_route = true
}
}
if let Some(query) = &route.query {
if query.contains_ident(field) {
from_route = true
}
}
if let Some(hash) = &route.hash {
if hash.contains_ident(field) {
from_route = true
}
}
}
},
RouteEndpoint::Redirect(redirect) => {
for segment in &redirect.segments {
if segment.name().as_ref() == Some(field) {
from_route = true
}
}
if let Some(query) = &redirect.query {
if query.contains_ident(field) {
from_route = true
}
}
if let Some(hash) = &redirect.hash {
if hash.contains_ident(field) {
from_route = true
}
}
}
}
}
from_route
}
fn impl_display(&self) -> TokenStream2 { fn impl_display(&self) -> TokenStream2 {
let mut display_match = Vec::new(); let mut display_match = Vec::new();
@@ -737,7 +664,7 @@ impl RouteEnum {
// Collect all routes matches // Collect all routes matches
for route in &self.endpoints { for route in &self.endpoints {
if let RouteEndpoint::Route(route) = route { if let RouteEndpoint::Route(route) = route {
matches.push(route.routable_match(&self.layouts, &self.nests)); matches.push(route.routable_match(&self.layouts, &self.nests, name));
} }
} }
+92 -8
View File
@@ -64,7 +64,7 @@ pub(crate) struct Route {
} }
impl Route { impl Route {
pub fn parse( pub(crate) fn parse(
nests: Vec<NestId>, nests: Vec<NestId>,
layouts: Vec<LayoutId>, layouts: Vec<LayoutId>,
variant: syn::Variant, variant: syn::Variant,
@@ -167,7 +167,7 @@ impl Route {
}) })
} }
pub fn display_match(&self, nests: &[Nest]) -> TokenStream2 { pub(crate) fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
let name = &self.route_name; let name = &self.route_name;
let dynamic_segments = self.dynamic_segments(); let dynamic_segments = self.dynamic_segments();
let write_query: Option<TokenStream2> = self.query.as_ref().map(|q| q.write()); let write_query: Option<TokenStream2> = self.query.as_ref().map(|q| q.write());
@@ -211,7 +211,12 @@ impl Route {
} }
} }
pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 { pub(crate) fn routable_match(
&self,
layouts: &[Layout],
nests: &[Nest],
router_name: &Ident,
) -> TokenStream2 {
let name = &self.route_name; let name = &self.route_name;
let mut tokens = TokenStream2::new(); let mut tokens = TokenStream2::new();
@@ -265,9 +270,85 @@ impl Route {
RouteType::Leaf { component } => { RouteType::Leaf { component } => {
let dynamic_segments = self.dynamic_segments(); let dynamic_segments = self.dynamic_segments();
let dynamic_segments_from_route = self.dynamic_segments(); let dynamic_segments_from_route = self.dynamic_segments();
/*
The implementation of this is pretty gnarly/gross.
We achieve the bundle splitting by wrapping the incoming function in a new component
that suspends based on an internal lazy loader. This lets us use suspense features
without breaking the rules of hooks. The router derive is quite complex so this shoves
the complexity towards the "leaf" of the codegen rather to its core. In the future though,
we should think about restructuring the router macro completely since its codegen
makes up nearly 30-40% of the binary size in the dioxus docsite.
*/
use sha2::Digest;
let dynamic_segments_receiver = self.dynamic_segments();
let dynamic_segments_from_route_ = self.dynamic_segments();
let dynamic_segments_from_route__ = self.dynamic_segments();
let unique_identifier = base16::encode_lower(
&sha2::Sha256::digest(format!("{name} {span:?}", span = name.span()))[..16],
);
let module_name = format_ident!("module{}{unique_identifier}", name).to_string();
let comp_name = format_ident!("route{}{unique_identifier}", name);
quote! { quote! {
#[allow(unused)] #[allow(unused)]
(#last_index, Self::#name { #(#dynamic_segments,)* }) => { (#last_index, Self::#name { #(#dynamic_segments,)* }) => {
dioxus::config_macros::maybe_wasm_split! {
if wasm_split {
{
fn #comp_name(args: #router_name) -> Element {
match args {
#router_name::#name { #(#dynamic_segments_from_route_,)* } => {
rsx! {
#component {
#(#dynamic_segments_from_route__: #dynamic_segments_from_route__,)*
}
}
}
_ => unreachable!()
}
}
#[component]
fn LoaderInner(args: NoPartialEq<#router_name>) -> Element {
static MODULE: wasm_split::LazyLoader<#router_name, Element> =
wasm_split::lazy_loader!(extern #module_name fn #comp_name(props: #router_name) -> Element);
use_resource(|| async move { MODULE.load().await }).suspend()?;
MODULE.call(args.0).unwrap()
}
struct NoPartialEq<T>(T);
impl<T: Clone> Clone for NoPartialEq<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: std::fmt::Display> std::fmt::Display for NoPartialEq<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<T> PartialEq for NoPartialEq<T> {
fn eq(&self, _other: &Self) -> bool {
false
}
}
rsx! {
LoaderInner {
args: NoPartialEq(#router_name::#name { #(#dynamic_segments_receiver,)* } )
}
}
}
} else {
{
rsx! { rsx! {
#component { #component {
#(#dynamic_segments_from_route: #dynamic_segments_from_route,)* #(#dynamic_segments_from_route: #dynamic_segments_from_route,)*
@@ -276,6 +357,9 @@ impl Route {
} }
} }
} }
}
}
}
}); });
tokens tokens
@@ -287,7 +371,7 @@ impl Route {
}) })
} }
pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 { pub(crate) fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
let segments = self.fields.iter().map(|(name, _)| { let segments = self.fields.iter().map(|(name, _)| {
let mut from_route = false; let mut from_route = false;
@@ -343,11 +427,11 @@ impl Route {
} }
} }
pub fn error_ident(&self) -> Ident { pub(crate) fn error_ident(&self) -> Ident {
format_ident!("{}ParseError", self.route_name) format_ident!("{}ParseError", self.route_name)
} }
pub fn error_type(&self) -> TokenStream2 { pub(crate) fn error_type(&self) -> TokenStream2 {
let error_name = self.error_ident(); let error_name = self.error_ident();
let child_type = match &self.ty { let child_type = match &self.ty {
RouteType::Child(field) => Some(&field.ty), RouteType::Child(field) => Some(&field.ty),
@@ -357,14 +441,14 @@ impl Route {
create_error_type(&self.route, error_name, &self.segments, child_type) create_error_type(&self.route, error_name, &self.segments, child_type)
} }
pub fn parse_query(&self) -> TokenStream2 { pub(crate) fn parse_query(&self) -> TokenStream2 {
match &self.query { match &self.query {
Some(query) => query.parse(), Some(query) => query.parse(),
None => quote! {}, None => quote! {},
} }
} }
pub fn parse_hash(&self) -> TokenStream2 { pub(crate) fn parse_hash(&self) -> TokenStream2 {
match &self.hash { match &self.hash {
Some(hash) => hash.parse(), Some(hash) => hash.parse(),
None => quote! {}, None => quote! {},
+1
View File
@@ -21,6 +21,7 @@ rustversion = "1.0.17"
[features] [features]
default = [] default = []
wasm-split = []
[dev-dependencies] [dev-dependencies]
axum = { workspace = true, features = ["ws"] } axum = { workspace = true, features = ["ws"] }
+26
View File
@@ -97,6 +97,32 @@ fn BlogPost(blog_id: usize) -> Element {
You need to enable the right features for the platform you're targeting since these are not determined automatically! You need to enable the right features for the platform you're targeting since these are not determined automatically!
## Bundle Splitting
The Dioxus Router supports automatic bundle splitting along route variants. To enable this, you need to manually turn on the `wasm-split` feature explicitly on the dioxus-router crate:
```toml
[dependencies]
dioxus = { version = "*", features = ["router", "wasm-split"] }
dioxus-router = { version = "*", features = ["wasm-split"] }
```
Note that `wasm-split` must also be turned on in dioxus since the macro uses the re-exported `wasm-split` from the dioxus prelude.
Enabling splitting disconnects the call graph, meaning if you try to run your app with a normal `dx serve`, it won't work. When running with router splitting, you need to pass `--experimental-wasm-split`.
```sh
dx serve --experimental-wasm-split
```
In practice, we recommend passing `dioxus-router?/wasm-split` as a feature only when bundling:
```sh
dx bundle --features "dioxus-router?/wasm-split" --experimental-wasm-split
```
Note that the router will call `.suspend()` so you should add a SuspenseBoundary above the Outlet to prevent suspending the entire page.
## Contributing ## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
+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);
}
}