diff --git a/.gitignore b/.gitignore index 6c75077d..14259829 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,12 @@ examples/wasm_demo_tsp/web/dist/ examples/wasm_demo_tsp/web/pkg/ examples/wasm_demo_tsp/crate/target/ +# wasm demo tsp generated artifacts +examples/wasm_demo_tsp2/web/node_modules/ +examples/wasm_demo_tsp2/web/dist/ +examples/wasm_demo_tsp2/web/pkg/ +examples/wasm_demo_tsp2/crate/target/ + # wasm vite demo basic generated artifacts examples/wasm_vite_demo_basic/web/node_modules/ examples/wasm_vite_demo_basic/web/dist/ diff --git a/Cargo.toml b/Cargo.toml index adf4995c..719e6370 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ wasm-web-threads = [ "dep:js-sys", "dep:wasm-bindgen-rayon", ] +wasm-web-threads2 = ["std", "dep:wasm-bindgen", "dep:js-sys"] # examples diff --git a/docs/wasm-plan-b.md b/docs/wasm-plan-b.md new file mode 100644 index 00000000..f6d1cf0a --- /dev/null +++ b/docs/wasm-plan-b.md @@ -0,0 +1,447 @@ +# Wasm Plan B: Remove rayon-core / wasm-bindgen-rayon Dependency for Web Threads + +This document proposes a migration path for wasm web-threaded execution that keeps current behavior intact while adding a new runtime path that does not depend on `rayon-core` or `wasm-bindgen-rayon`. + +## Goals + +1. Keep existing implementation and feature: +- existing module: `src/pool/pool_impl/wasm_web.rs` +- existing feature: `wasm-web-threads` + +2. Add new implementation and feature: +- new module: `src/pool/pool_impl/wasm_web2.rs` +- new feature: `wasm-web-threads2` + +3. New path (`wasm-web-threads2`) must not depend on: +- `rayon-core` +- `wasm-bindgen-rayon` + +4. When new feature is active on wasm, make new pool the default: +- `DefaultPool = WasmWebPool2` under `wasm-web-threads2` + +5. Preserve startup model inspired by current wasm flow: +- call and await `init_thread_pool(...)` before parallel runs + +6. Keep user-facing usage similar to `wasm_demo_tsp`: +- same high-level API and startup sequence +- only underlying pool/runtime implementation changes + +7. Add a new demo: +- `examples/wasm_demo_tsp2` + +## Current State Snapshot + +- Feature wiring currently enables wasm web-thread support via `wasm-web-threads` in `Cargo.toml` and pulls in `rayon-core` and `wasm-bindgen-rayon`. +- `DefaultPool` selection currently maps wasm-threaded builds to `WasmWebPool` in `src/pool/mod.rs`. +- `WasmWebPool` in `src/pool/pool_impl/wasm_web.rs` uses: + - `rayon_core::scope` for scoped execution + - `wasm_bindgen_rayon::init_thread_pool(...)` for initialization +- `wasm_demo_tsp` startup sequence already follows this model: + 1. wasm module init + 2. await runtime init function + 3. run parallel computation + +## Proposed Architecture + +Keep the `ParThreadPool` abstraction unchanged and add a second wasm backend implementation. + +- Legacy backend (unchanged): + - feature: `wasm-web-threads` + - module: `wasm_web.rs` + - runtime: rayon-backed + +- New backend: + - feature: `wasm-web-threads2` + - module: `wasm_web2.rs` + - runtime: internal worker/thread runtime and scheduler, no rayon dependencies + +This preserves external API style while allowing internal implementation replacement. + +## Phase Plan + +## Phase 1: Feature Graph and Conditional Exports + +### 1. Cargo feature additions (`Cargo.toml`) + +- Add `wasm-web-threads2` feature. +- Include only wasm interop dependencies required by the new runtime. +- Ensure `wasm-web-threads2` does not include `rayon-core` / `wasm-bindgen-rayon`. +- Keep `wasm-web-threads` unchanged for backward compatibility. + +### 2. Pool module exports (`src/pool/pool_impl/mod.rs`, `src/pool/mod.rs`) + +- Add gated module/export for `WasmWebPool2`. +- Add gated re-export for `init_thread_pool` under `wasm-web-threads2`. +- Introduce deterministic feature conflict behavior when both wasm features are enabled: + - recommended: compile-time error to enforce one backend per build. + +### 3. Default pool precedence (`src/pool/mod.rs`) + +- Set `DefaultPool = WasmWebPool2` under: + - `target_arch = "wasm32"` + - `feature = "wasm-web-threads2"` +- Keep existing `DefaultPool` paths for other targets/features. + +### 4. Pool factory API (`src/pool/new_pool.rs`) + +- Add `Pool::wasm_web2(...) -> WasmWebPool2`. +- Keep `Pool::wasm_web(...)` intact. + +## Phase 2: New Wasm Runtime Bootstrap (No Rayon) + +Implement a minimal internal runtime bootstrap for web workers. + +### Responsibilities + +- Provide `init_thread_pool(num_threads) -> js_sys::Promise`. +- Initialize worker infrastructure and shared memory/thread metadata. +- Persist initialized state (fail-fast if not initialized before parallel use). +- Preserve atomics-required safety checks and error messages. + +### Design guidance + +- Follow lifecycle choices similar to `wasm-bindgen-rayon`: + - one-time async initialization at startup + - runtime ready gate before any scoped parallel work +- Keep implementation internal to `orx-parallel`; no rayon runtime dependency. + +## Phase 3: Implement `WasmWebPool2` (`src/pool/pool_impl/wasm_web2.rs`) + +Implement `ParThreadPool` for new wasm backend. + +### Required behavior + +- Scoped task execution semantics compatible with current iterator runners. +- Respect existing thread-count logic: + - `NumThreads::Auto` + - `NumThreads::Max(n)` + - upper-bound handling via `ParThreadPool::max_num_threads_for_computation` +- Panic/fail-fast behavior when runtime is uninitialized. + +### API compatibility + +- Preserve the same high-level usage pattern in user code: + - optional explicit pool via `.pool(Pool::wasm_web2(...))` + - default pool path when `wasm-web-threads2` is active + +## Phase 4: Demo Compatibility (`examples/wasm_demo_tsp2`) + +Create a second demo mirroring `wasm_demo_tsp` ergonomics while using the new backend. + +### Deliverables + +- New crate and web app under `examples/wasm_demo_tsp2`. +- Demo crate depends on: + - `orx-parallel` with `wasm-web-threads2` +- Expose startup initializer in wasm boundary layer (same pattern): + - `init_parallel_runtime(...)` calling `orx_parallel::init_thread_pool(...)` +- Frontend startup sequence remains: + 1. `await init()` + 2. `await init_parallel_runtime(...)` + 3. enable parallel actions + +## Phase 5: Tests and Validation + +## 1. Compile-time matrix + +- Native path unchanged. +- Legacy wasm path (`wasm-web-threads`) unchanged. +- New wasm path (`wasm-web-threads2`) builds without rayon dependencies. + +## 2. Behavior smoke tests + +- panic/fail path before initialization +- success path after initialization +- thread-cap behavior and `num_threads` interactions +- `DefaultPool` routing under `wasm-web-threads2` + +## 3. Runtime demo validation + +- Run `wasm_demo_tsp2` under COOP/COEP headers. +- Validate that parallel runs complete and UI behavior matches expectations. + +## Phase 6: Documentation and Rollout + +- Add docs page for new backend (or extend existing wasm docs with dual-backend section). +- Clearly mark `wasm-web-threads` as legacy/compat backend once `wasm-web-threads2` stabilizes. +- Keep both features for at least one transition release. + +## Suggested Work Order + +1. Feature and cfg wiring +- `Cargo.toml` +- `src/pool/pool_impl/mod.rs` +- `src/pool/mod.rs` +- `src/pool/new_pool.rs` + +2. Scaffold new backend +- `src/pool/pool_impl/wasm_web2.rs` +- internal helper modules as needed + +3. Add tests +- wasm smoke tests for init contract and pool behavior + +4. Add new demo +- `examples/wasm_demo_tsp2` + +5. Docs update +- startup contract, build flags, feature selection guidance + +## Risks and Mitigations + +## Risk 1: Worker runtime bootstrap complexity + +- Mitigation: keep first version minimal, copy only essential design patterns from existing proven crates, iterate. + +## Risk 2: Bundler/runtime variations + +- Mitigation: first target same `wasm-pack --target web` path used by existing demo, then expand compatibility. + +## Risk 3: Feature interaction ambiguity + +- Mitigation: explicit cfg guards and compile-time conflict checks. + +## Risk 4: Behavioral regressions in scheduling semantics + +- Mitigation: add focused smoke tests for initialization, scope completion, panic propagation, and thread caps. + +## Acceptance Criteria + +- `wasm-web-threads2` works on wasm web-thread builds without `rayon-core`/`wasm-bindgen-rayon`. +- `DefaultPool` resolves to new wasm pool when `wasm-web-threads2` is active. +- Startup and usage model remain similar to current `wasm_demo_tsp`. +- `wasm_demo_tsp2` demonstrates equivalent user-facing behavior. +- Existing `wasm-web-threads` path remains functional during transition. + +## PR-Ready Task Breakdown + +This section converts the plan into mergeable PR slices with explicit boundaries. + +## Branching and Merge Strategy + +- Base branch: `main` +- Feature branch prefix: `feat/wasm-web-threads2-*` +- Merge order: PR-1 -> PR-2 -> PR-3 -> PR-4 -> PR-5 -> PR-6 +- Keep each PR independently reviewable and green in CI. + +## PR-1: Feature Wiring and Public Surface Scaffold + +### Objective + +Introduce feature flags and exports for the new backend without implementing runtime logic yet. + +### Scope + +- Add `wasm-web-threads2` feature in `Cargo.toml`. +- Add module gates and re-exports for `WasmWebPool2` and `init_thread_pool`. +- Add `Pool::wasm_web2(...)` factory method. +- Add deterministic feature conflict guard if both wasm backends are enabled. + +### Files + +- `Cargo.toml` +- `src/pool/mod.rs` +- `src/pool/pool_impl/mod.rs` +- `src/pool/new_pool.rs` +- `src/pool/pool_impl/wasm_web2.rs` (stub only; compile placeholder) + +### Validation + +- `cargo check --features std` +- `cargo check --target wasm32-unknown-unknown --features wasm-web-threads2` +- `cargo check --target wasm32-unknown-unknown --features wasm-web-threads,wasm-web-threads2` should fail with intended compile-time conflict message. + +### Definition of Done + +- New feature is visible and wired. +- Build succeeds for `wasm-web-threads2` target path. +- No runtime behavior change yet. + +## PR-2: Runtime Bootstrap API (Init Contract) + +### Objective + +Implement the initialization contract for wasm-web-threads2, including one-time startup and readiness checks. + +### Scope + +- Implement `init_thread_pool(num_threads) -> js_sys::Promise` for new backend. +- Add one-time init state and fail-fast readiness checks. +- Preserve atomics-required panic/diagnostic behavior. + +### Files + +- `src/pool/pool_impl/wasm_web2.rs` +- optional helper modules under `src/pool/pool_impl/` if needed +- docs updates for init lifecycle notes + +### Validation + +- compile checks from PR-1 +- targeted wasm smoke compile/tests for init-uninitialized and init-success paths (or no-run compile smoke if runtime environment is unavailable in CI) + +### Definition of Done + +- Startup API exists and is callable from wasm boundary. +- Uninitialized usage has clear fail-fast behavior. + +## PR-3: WasmWebPool2 ParThreadPool Execution + +### Objective + +Implement scoped execution semantics for the new pool backend. + +### Scope + +- Implement `ParThreadPool` for `WasmWebPool2` and `&WasmWebPool2`. +- Ensure `max_num_threads` semantics align with `NumThreads::Auto` and `NumThreads::Max(n)`. +- Ensure compatibility with existing runner expectations (scoped task scheduling and completion semantics). + +### Files + +- `src/pool/pool_impl/wasm_web2.rs` +- potentially shared runtime helper module(s) + +### Validation + +- existing wasm check commands +- representative parallel iterator smoke tests under wasm target: + - `map + reduce` + - `find/first` short-circuit paths + - error path before init + +### Definition of Done + +- New backend can run parallel tasks with correct scope completion behavior. +- Thread cap logic behaves as expected. + +## PR-4: DefaultPool Routing and Compatibility Guarantees + +### Objective + +Switch wasm default pool routing to the new backend under `wasm-web-threads2` and verify compatibility. + +### Scope + +- Make `DefaultPool` resolve to `WasmWebPool2` when `wasm-web-threads2` is active on wasm32. +- Confirm legacy route remains unchanged under `wasm-web-threads`. +- Add/adjust tests that assert feature-based pool selection behavior. + +### Files + +- `src/pool/mod.rs` +- tests related to cfg/feature routing + +### Validation + +- compile matrix: + - native default features + - wasm with `wasm-web-threads` + - wasm with `wasm-web-threads2` +- smoke assertions for default pool behavior under each configuration + +### Definition of Done + +- New backend is default only when intended. +- No regression for legacy backend path. + +## PR-5: New Demo wasm_demo_tsp2 + +### Objective + +Provide a user-facing demonstration that mirrors existing demo ergonomics with the new backend. + +### Scope + +- Add `examples/wasm_demo_tsp2` with crate + web structure. +- Use `orx-parallel` with `wasm-web-threads2` in demo crate. +- Expose `init_parallel_runtime(...)` and parallel/sequential run functions following existing demo style. +- Keep frontend startup sequence aligned with current demo. + +### Files + +- `examples/wasm_demo_tsp2/**` + +### Validation + +- demo crate compiles for wasm target +- web app starts and performs parallel run after awaited init +- visual/interaction parity checks against existing demo behavior + +### Definition of Done + +- Demo clearly validates plan goals and API ergonomics. + +## PR-6: Docs, Migration Notes, and Final Hardening + +### Objective + +Finalize operator guidance, migration path, and verification notes. + +### Scope + +- Update wasm docs to explain dual backend features and when to use each. +- Add migration guidance from `wasm-web-threads` to `wasm-web-threads2`. +- Document known limitations and troubleshooting for new backend. +- Add release-note draft bullets. + +### Files + +- `docs/wasm.md` +- `docs/wasm_web_threads.md` (or split into legacy/new pages) +- `README.md` sections as needed +- `docs/wasm-plan-b.md` (mark implementation status) + +### Validation + +- docs examples compile or are at least command-validated where possible +- commands and feature names are consistent with code + +### Definition of Done + +- Users can adopt new backend from docs without code archaeology. + +## Global Checklist (Tracking) + +- [ ] PR-1 merged +- [ ] PR-2 merged +- [ ] PR-3 merged +- [ ] PR-4 merged +- [ ] PR-5 merged +- [ ] PR-6 merged +- [ ] CI green for native + wasm legacy + wasm2 paths +- [ ] Demo wasm_demo_tsp2 validated in browser runtime + +## Reviewer Checklist Per PR + +- [ ] Feature gates are minimal and correct. +- [ ] No accidental dependency on `rayon-core` or `wasm-bindgen-rayon` under `wasm-web-threads2`. +- [ ] Public API changes are documented. +- [ ] Error messages are actionable. +- [ ] Test/compile commands are included in PR description. + +## Suggested PR Template Snippet + +Use this in each PR description. + +```md +## Summary +- + +## Scope +- + +## Out of Scope +- + +## Validation +- [ ] cargo check ... +- [ ] wasm target check ... +- [ ] smoke test ... + +## Risks +- + +## Follow-ups +- +``` diff --git a/examples/wasm_demo_tsp2/README.md b/examples/wasm_demo_tsp2/README.md new file mode 100644 index 00000000..3be7e72a --- /dev/null +++ b/examples/wasm_demo_tsp2/README.md @@ -0,0 +1,25 @@ +# orx-parallel wasm demo tsp2 + +This demo shows the `wasm-web-threads2` path for running `orx-parallel` in the browser with wasm threads. + +It mirrors `examples/wasm_demo_tsp` in structure and behavior, but uses the new backend that does not depend on `rayon-core` or `wasm-bindgen-rayon`. + +## What this demo contains + +- `crate/`: Rust compute module compiled to wasm. +- `web/`: Vite + TypeScript UI that loads wasm, initializes the thread pool, and runs search jobs. + +## How to run + +Use the same workflow as the existing demo: + +1. Install prerequisites. +2. Build the wasm package from `web/`. +3. Start the Vite dev server. +4. Open the printed URL and run parallel or sequential search. + +The runtime startup flow is the same: + +1. `await init()` +2. `await init_parallel_runtime(...)` +3. run parallel computations diff --git a/examples/wasm_demo_tsp2/crate/Cargo.toml b/examples/wasm_demo_tsp2/crate/Cargo.toml new file mode 100644 index 00000000..e038c73a --- /dev/null +++ b/examples/wasm_demo_tsp2/crate/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "orx-parallel-wasm-demo-tsp2" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +derive-new = { version = "0.7.0", default-features = false } +orx-parallel = { path = "../../..", features = ["wasm-web-threads2"] } +wasm-bindgen = "0.2" +js-sys = "0.3" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +rand = { version = "0.9", default-features = false, features = [ + "std", + "small_rng", +] } + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false diff --git a/examples/wasm_demo_tsp2/crate/src/computation.rs b/examples/wasm_demo_tsp2/crate/src/computation.rs new file mode 100644 index 00000000..f6d8188a --- /dev/null +++ b/examples/wasm_demo_tsp2/crate/src/computation.rs @@ -0,0 +1,102 @@ +use crate::locations::Location; +use core::cmp::Ordering::Equal; +use orx_parallel::*; +use rand::prelude::*; +use rand::rngs::SmallRng; + +/// Output of a single search chunk, including best candidate and timing info. +pub struct SearchRunOutput { + pub best: Option<(Vec, f64)>, + pub iterations: usize, +} + +/// Runs a sequential TSP search chunk and returns best/timing metadata. +pub fn run_search_sequential( + iterations: usize, + seed: u64, + locations: &[Location], + start_index: u64, +) -> SearchRunOutput { + let best = (0..iterations) + .map(|k| search_candidate(seed, start_index.wrapping_add(k as u64), locations)) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Equal)); + + SearchRunOutput { best, iterations } +} + +/// Runs a parallel TSP search chunk and returns best/timing metadata. +pub fn run_search_parallel( + iterations: usize, + seed: u64, + threads: usize, + locations: &[Location], + start_index: u64, +) -> SearchRunOutput { + let best = (0..iterations) + .into_par() + .num_threads(threads) + .map(|k| search_candidate(seed, start_index.wrapping_add(k as u64), locations)) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Equal)); + + SearchRunOutput { best, iterations } +} + +fn search_candidate(seed: u64, k: u64, locations: &[Location]) -> (Vec, f64) { + let tour = random_tour( + seed ^ k.wrapping_mul(0x9E37_79B9_7F4A_7C15), + locations.len(), + ); + let tour = two_opt_improve(locations, tour); + let distance = Location::tour_distance(locations, &tour); + (tour, distance) +} + +fn random_tour(seed: u64, num_cities: usize) -> Vec { + let mut tour: Vec = (0..num_cities).collect(); + let mut rng = SmallRng::seed_from_u64(seed); + tour.shuffle(&mut rng); + tour +} + +fn two_opt_improve(locations: &[Location], mut tour: Vec) -> Vec { + let edge_distance = |i: usize, j: usize| locations[i].distance_to(locations[j]); + + let n = tour.len(); + if n < 4 { + return tour; + } + + let mut improved = true; + while improved { + improved = false; + + for i in 0..(n - 1) { + let a = tour[i]; + let b = tour[(i + 1) % n]; + + for j in (i + 2)..n { + if i == 0 && j == n - 1 { + continue; + } + + let c = tour[j]; + let d = tour[(j + 1) % n]; + + let current = edge_distance(a, b) + edge_distance(c, d); + let swapped = edge_distance(a, c) + edge_distance(b, d); + + if swapped + 1e-12 < current { + tour[(i + 1)..=j].reverse(); + improved = true; + break; + } + } + + if improved { + break; + } + } + } + + tour +} diff --git a/examples/wasm_demo_tsp2/crate/src/lib.rs b/examples/wasm_demo_tsp2/crate/src/lib.rs new file mode 100644 index 00000000..c30a6a37 --- /dev/null +++ b/examples/wasm_demo_tsp2/crate/src/lib.rs @@ -0,0 +1,154 @@ +use serde::Serialize; +use wasm_bindgen::prelude::*; + +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +mod computation; +mod locations; + +#[cfg_attr( + not(all(target_arch = "wasm32", target_feature = "atomics")), + allow(dead_code) +)] +#[derive(Debug, Serialize)] +struct RunResult { + best_tour: Vec, + best_distance: f64, + iterations: usize, + elapsed_ms: f64, +} + +#[cfg_attr( + not(all(target_arch = "wasm32", target_feature = "atomics")), + allow(dead_code) +)] +#[derive(Debug, Serialize)] +struct RuntimeInfo { + configured_threads: usize, + spawned_workers: usize, +} + +/// Initializes the wasm worker thread pool used by parallel runs. +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[wasm_bindgen] +pub fn init_parallel_runtime(num_threads: u32) -> js_sys::Promise { + orx_parallel::init_thread_pool(num_threads as usize) +} + +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[wasm_bindgen] +pub fn parallel_runtime_info() -> Result { + let (configured_threads, spawned_workers) = orx_parallel::wasm_web2_runtime_info(); + let info = RuntimeInfo { + configured_threads, + spawned_workers, + }; + serde_wasm_bindgen::to_value(&info) + .map_err(|e| JsValue::from_str(&format!("failed to serialize runtime info: {e}"))) +} + +#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] +#[wasm_bindgen] +pub fn parallel_runtime_info() -> Result { + Err(JsValue::from_str( + "parallel_runtime_info is only available for wasm32 + atomics builds", + )) +} + +/// Returns an error when thread-pool initialization is unavailable on this target. +#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] +#[wasm_bindgen] +pub fn init_parallel_runtime(_num_threads: u32) -> Result { + Err(JsValue::from_str( + "init_parallel_runtime is only available for wasm32 + atomics builds", + )) +} + +/// Returns the city coordinates for the requested problem size. +#[wasm_bindgen] +pub fn locations(num_cities: u32) -> Result { + let locations = locations::locations(num_cities); + serde_wasm_bindgen::to_value(&locations) + .map_err(|e| JsValue::from_str(&format!("failed to serialize locations: {e}"))) +} + +/// Runs a parallel TSP search chunk and returns the best tour found in that chunk. +#[wasm_bindgen] +pub fn run_best_tour_par( + iterations: u32, + seed: u64, + threads: u32, + num_cities: u32, + start_index: u64, +) -> Result { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + { + let _ = (iterations, seed, threads, num_cities, start_index); + return Err(JsValue::from_str( + "run_best_tour_par requires wasm32 + atomics build", + )); + } + + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + { + let iterations = iterations.max(1) as usize; + let threads = threads.max(1) as usize; + let num_cities = locations::clamp_num_cities(num_cities); + let locations = locations::locations(num_cities as u32); + let started_at = js_sys::Date::now(); + let output = + computation::run_search_parallel(iterations, seed, threads, &locations, start_index); + let elapsed_ms = js_sys::Date::now() - started_at; + run_output_to_js(output, elapsed_ms) + } +} + +/// Runs a sequential TSP search chunk and returns the best tour found in that chunk. +#[wasm_bindgen] +pub fn run_best_tour_seq( + iterations: u32, + seed: u64, + num_cities: u32, + start_index: u64, +) -> Result { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + { + let _ = (iterations, seed, num_cities, start_index); + return Err(JsValue::from_str( + "run_best_tour_seq requires wasm32 + atomics build", + )); + } + + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + { + let iterations = iterations.max(1) as usize; + let num_cities = locations::clamp_num_cities(num_cities); + let locations = locations::locations(num_cities as u32); + let started_at = js_sys::Date::now(); + let output = computation::run_search_sequential(iterations, seed, &locations, start_index); + let elapsed_ms = js_sys::Date::now() - started_at; + run_output_to_js(output, elapsed_ms) + } +} + +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +fn run_output_to_js( + output: computation::SearchRunOutput, + elapsed_ms: f64, +) -> Result { + match output.best { + Some((best_tour, best_distance)) => { + let result = RunResult { + best_tour, + best_distance, + iterations: output.iterations, + elapsed_ms, + }; + + serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsValue::from_str(&format!("failed to serialize result: {e}"))) + } + None => Err(JsValue::from_str( + "no tour could be generated (unexpected empty search)", + )), + } +} diff --git a/examples/wasm_demo_tsp2/crate/src/locations.rs b/examples/wasm_demo_tsp2/crate/src/locations.rs new file mode 100644 index 00000000..d3a9fb62 --- /dev/null +++ b/examples/wasm_demo_tsp2/crate/src/locations.rs @@ -0,0 +1,77 @@ +use serde::Serialize; + +const MIN_CITIES: usize = 5; +const MAX_CITIES: usize = 200; + +#[derive(Clone, Copy, Debug, Serialize)] +/// A 2D city coordinate used by the TSP demo. +pub struct Location { + pub x: f64, + pub y: f64, +} + +impl Location { + #[cfg_attr( + not(all(target_arch = "wasm32", target_feature = "atomics")), + allow(dead_code) + )] + pub fn distance_to(self, other: Self) -> f64 { + let dx = self.x - other.x; + let dy = self.y - other.y; + (dx * dx + dy * dy).sqrt() + } + + #[cfg_attr( + not(all(target_arch = "wasm32", target_feature = "atomics")), + allow(dead_code) + )] + pub fn tour_distance(locations: &[Location], tour: &[usize]) -> f64 { + match (tour.first(), tour.last()) { + (Some(&first), Some(&last)) => { + let middle_distance: f64 = tour + .windows(2) + .map(|w| locations[w[0]].distance_to(locations[w[1]])) + .sum(); + let closing_distance = locations[last].distance_to(locations[first]); + middle_distance + closing_distance + } + _ => 0.0, + } + } +} + +/// Returns city coordinates for the requested city count. +pub fn locations(num_cities: u32) -> Vec { + let num_cities = clamp_num_cities(num_cities); + (0..num_cities).map(location_for).collect() +} + +/// Clamps a requested city count into the supported demo range. +pub fn clamp_num_cities(num_cities: u32) -> usize { + (num_cities as usize).clamp(MIN_CITIES, MAX_CITIES) +} + +/// Returns the deterministic pseudo-random location for a city index. +pub fn location_for(idx: usize) -> Location { + // Deterministic pseudo-random coordinates: random-looking, but stable per index. + let sx = split_mix_64((idx as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)); + let sy = split_mix_64((idx as u64).wrapping_mul(0xD1B5_4A32_D192_ED03)); + + let x = 100.0 * to_unit_f64(sx) - 50.0; + let y = 100.0 * to_unit_f64(sy) - 50.0; + Location { x, y } +} + +fn split_mix_64(mut x: u64) -> u64 { + x = x.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = x; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} + +fn to_unit_f64(x: u64) -> f64 { + // Keep top 53 bits for an exact f64 mantissa and map to [0, 1). + let v = x >> 11; + (v as f64) * (1.0 / ((1u64 << 53) as f64)) +} diff --git a/examples/wasm_demo_tsp2/web/index.html b/examples/wasm_demo_tsp2/web/index.html new file mode 100644 index 00000000..086ba300 --- /dev/null +++ b/examples/wasm_demo_tsp2/web/index.html @@ -0,0 +1,62 @@ + + + + + + orx-parallel wasm demo tsp2 + + + +
+

orx-parallel in browser wasm

+

Find a short tour across points by applying local search (2-opt) on randomly created tours.

+
+ +
Initializing...
+
+ + + + +
+
+ + + +
+
+

Best Distance

-

+

Elapsed

-

+

Iterations/s

-

+
+ +
+
+ + + diff --git a/examples/wasm_demo_tsp2/web/package-lock.json b/examples/wasm_demo_tsp2/web/package-lock.json new file mode 100644 index 00000000..b0f01aee --- /dev/null +++ b/examples/wasm_demo_tsp2/web/package-lock.json @@ -0,0 +1,1002 @@ +{ + "name": "orx-parallel-wasm-demo-tsp2-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "orx-parallel-wasm-demo-tsp2-web", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://artifactory.dhl.com/artifactory/api/npm/npm-remote/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/examples/wasm_demo_tsp2/web/package.json b/examples/wasm_demo_tsp2/web/package.json new file mode 100644 index 00000000..528ddd58 --- /dev/null +++ b/examples/wasm_demo_tsp2/web/package.json @@ -0,0 +1,17 @@ +{ + "name": "orx-parallel-wasm-demo-tsp2-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build:wasm": "RUSTUP_TOOLCHAIN=nightly RUSTFLAGS='-C target-feature=+atomics,+bulk-memory -C link-arg=--shared-memory -C link-arg=--max-memory=1073741824 -C link-arg=--import-memory -C link-arg=--export=__wasm_init_tls -C link-arg=--export=__tls_size -C link-arg=--export=__tls_align -C link-arg=--export=__tls_base' wasm-pack build ../crate --target web --out-dir ../web/pkg -- -Z build-std=panic_abort,std", + "dev:full": "npm run build:wasm && vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} \ No newline at end of file diff --git a/examples/wasm_demo_tsp2/web/public/_headers b/examples/wasm_demo_tsp2/web/public/_headers new file mode 100644 index 00000000..c3720e51 --- /dev/null +++ b/examples/wasm_demo_tsp2/web/public/_headers @@ -0,0 +1,3 @@ +/* + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp diff --git a/examples/wasm_demo_tsp2/web/src/main.ts b/examples/wasm_demo_tsp2/web/src/main.ts new file mode 100644 index 00000000..e20a3351 --- /dev/null +++ b/examples/wasm_demo_tsp2/web/src/main.ts @@ -0,0 +1,476 @@ +import init, { + init_parallel_runtime, + locations, + parallel_runtime_info, + run_best_tour_par, + run_best_tour_seq +} from "../pkg/orx_parallel_wasm_demo_tsp2.js"; + +type RuntimeInfo = { + configured_threads: number; + spawned_workers: number; +}; + +type Location = { x: number; y: number }; +type SearchChunkResult = { + best_tour: number[]; + best_distance: number; + iterations: number; + elapsed_ms: number; +}; +type SearchMode = "parallel" | "sequential"; + +type RunSettings = { + mode: SearchMode; + iterations: number; + threads: number; + seed: bigint; + numCities: number; +}; + +type RunAggregate = { + best_tour: number[]; + best_distance: number; + iterations: number; + elapsed_ms: number; +}; + +function mustElement(id: string): T { + const el = document.getElementById(id); + if (!el) { + throw new Error(`missing required element: #${id}`); + } + return el as T; +} + +const ui = { + status: mustElement("status"), + iterations: mustElement("iterations"), + threads: mustElement("threads"), + seed: mustElement("seed"), + numCities: mustElement("numCities"), + runParallel: mustElement("runParallel"), + runSequential: mustElement("runSequential"), + reset: mustElement("reset"), + runOverlay: mustElement("runOverlay"), + runTitle: mustElement("runTitle"), + runSubtitle: mustElement("runSubtitle"), + runElapsed: mustElement("runElapsed"), + cancelRun: mustElement("cancelRun"), + bestDistance: mustElement("bestDistance"), + elapsed: mustElement("elapsed"), + ips: mustElement("ips"), + canvas: mustElement("canvas") +}; + +const maybeCtx = ui.canvas.getContext("2d"); + +if (!maybeCtx) { + throw new Error("failed to acquire canvas 2D context"); +} + +const ctx = maybeCtx; + +const MIN_CITIES = 5; +const MAX_CITIES = 200; +const MIN_THREADS = 1; +const MAX_THREADS = 16; +const STARTUP_PARALLEL_RUNTIME_THREADS = 16; +const PARALLEL_RUNTIME_INIT_TIMEOUT_MS = 12_000; +const state = { + points: [] as Location[], + threadPoolReady: false, + bestSoFar: null as RunAggregate | null, + currentNumCities: MAX_CITIES, + isRunning: false, + activeRunId: 0, + runTicker: undefined as number | undefined, + runStartedAtMs: 0, + cancelRequested: false +}; + +// Bootstraps wasm module, initial data and UI event wiring. +async function setupApp() { + await init(); + + state.currentNumCities = readNumCities(); + state.points = locations(state.currentNumCities) as Location[]; + drawPoints(state.points); + + wireUiHandlers(); + + ui.status.textContent = "Initializing parallel runtime..."; + void initParallelRuntimeInBackground(); +} + +function wireUiHandlers() { + + ui.runParallel.addEventListener("click", async () => { + await runSearch("parallel"); + }); + + ui.runSequential.addEventListener("click", async () => { + await runSearch("sequential"); + }); + + ui.reset.addEventListener("click", () => { + clearBest(); + drawPoints(state.points); + ui.status.textContent = "Best tour reset. Ready for a fresh run."; + }); + + ui.numCities.addEventListener("change", () => { + const numCities = readNumCities(); + state.points = locations(numCities) as Location[]; + clearBest(); + drawPoints(state.points); + ui.status.textContent = `Updated problem size to ${numCities} cities.`; + }); + + ui.threads.addEventListener("change", () => { + const threads = readThreads(); + ui.status.textContent = `Thread limit set to ${threads}.`; + }); + + ui.cancelRun.addEventListener("click", () => { + state.cancelRequested = true; + ui.cancelRun.disabled = true; + ui.runSubtitle.textContent = "Cancelling... this takes effect after the current chunk finishes."; + }); +} + +async function initParallelRuntimeInBackground() { + try { + await promiseWithTimeout( + init_parallel_runtime(STARTUP_PARALLEL_RUNTIME_THREADS), + PARALLEL_RUNTIME_INIT_TIMEOUT_MS, + "parallel runtime init timed out" + ); + state.threadPoolReady = true; + + try { + const runtimeInfo = parallel_runtime_info() as RuntimeInfo; + if (runtimeInfo.spawned_workers > 0) { + ui.status.textContent = `Ready. Parallel runtime configured=${runtimeInfo.configured_threads}, spawned_workers=${runtimeInfo.spawned_workers}.`; + } else { + ui.status.textContent = `Ready with fallback: configured=${runtimeInfo.configured_threads}, spawned_workers=0. Parallel runs will execute inline on this platform.`; + } + } catch (diagErr) { + console.warn("parallel_runtime_info failed", diagErr); + ui.status.textContent = "Ready. Parallel runtime initialized, but worker diagnostics are unavailable."; + } + } catch (err) { + state.threadPoolReady = false; + ui.status.textContent = `Parallel runtime init failed: ${String(err)}. Sequential mode remains available.`; + } +} + +function promiseWithTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => { + window.setTimeout(() => reject(new Error(message)), timeoutMs); + }) + ]); +} + +function readRunSettings(mode: SearchMode): RunSettings { + const iterations = Math.max(1, Number(ui.iterations.value) || 1); + const threads = readThreads(); + const seedInput = Math.max(1, Number(ui.seed.value) || 1); + return { + mode, + iterations, + threads, + seed: BigInt(Math.trunc(seedInput)), + numCities: readNumCities() + }; +} + +function setControlsDisabled(disabled: boolean) { + ui.runParallel.disabled = disabled; + ui.runSequential.disabled = disabled; + ui.reset.disabled = disabled; + ui.iterations.disabled = disabled; + ui.threads.disabled = disabled; + ui.seed.disabled = disabled; + ui.numCities.disabled = disabled; +} + +function ensurePointsForCities(numCities: number) { + if (state.points.length === numCities) { + return; + } + + state.points = locations(numCities) as Location[]; + clearBest(); + drawPoints(state.points); +} + +async function runSearch(mode: SearchMode) { + if (state.isRunning) { + ui.status.textContent = "A search is already running. Please wait for it to finish or cancel it."; + return; + } + + state.isRunning = true; + const runId = ++state.activeRunId; + + const settings = readRunSettings(mode); + ensurePointsForCities(settings.numCities); + + setControlsDisabled(true); + setRunningView(settings.mode, true); + await allowRunningOverlayToRender(); + + ui.status.textContent = settings.mode === "parallel" ? "Running parallel search..." : "Running sequential search..."; + + let startIndex = 0; + let runElapsedMs = 0; + let runBest: SearchChunkResult | null = null; + + try { + if (settings.mode === "parallel" && !state.threadPoolReady) { + throw new Error("parallel runtime is not initialized"); + } + + let remaining = settings.iterations; + const chunkSize = chooseChunkSize(settings.mode, settings.numCities, settings.iterations); + + while (remaining > 0) { + if (state.cancelRequested) { + break; + } + + const thisChunk = Math.min(remaining, chunkSize); + const chunkResult = runSearchChunk(settings, thisChunk, startIndex); + + runElapsedMs += chunkResult.elapsed_ms; + startIndex += thisChunk; + remaining -= thisChunk; + + const isRunBest = !runBest || chunkResult.best_distance < runBest.best_distance; + if (isRunBest) { + runBest = chunkResult; + } + + const currentRunBest = runBest ?? chunkResult; + + if (!state.bestSoFar || currentRunBest.best_distance < state.bestSoFar.best_distance) { + state.bestSoFar = toAggregate(currentRunBest, startIndex, runElapsedMs); + drawTour(state.points, state.bestSoFar.best_tour); + } + + ui.runSubtitle.textContent = `Processed ${startIndex.toLocaleString()} / ${settings.iterations.toLocaleString()} iterations...`; + await nextPaint(); + } + + if (startIndex === 0) { + throw new Error(`${settings.mode} run produced no work`); + } + + if (!runBest) { + throw new Error(`${settings.mode} run produced no result`); + } + + const summary = toAggregate(runBest, startIndex, runElapsedMs); + updateStats(summary); + + if (state.cancelRequested) { + ui.status.textContent = `${settings.mode === "parallel" ? "Parallel" : "Sequential"} run cancelled after ${startIndex.toLocaleString()} iterations.`; + } else if (state.bestSoFar && runBest.best_distance <= state.bestSoFar.best_distance) { + ui.status.textContent = `${settings.mode === "parallel" ? "Parallel" : "Sequential"} run completed (${startIndex.toLocaleString()} iterations, ${runElapsedMs.toFixed(1)} ms).`; + } else { + ui.status.textContent = `${settings.mode === "parallel" ? "Parallel" : "Sequential"} run completed (${startIndex.toLocaleString()} iterations, ${runElapsedMs.toFixed(1)} ms); best tour unchanged.`; + } + } catch (err) { + console.error("runSearch failed", err); + if (runBest && startIndex > 0) { + const partial = toAggregate(runBest, startIndex, runElapsedMs); + updateStats(partial); + ui.status.textContent = `Error in ${settings.mode} run after ${startIndex.toLocaleString()} iterations: ${String(err)}`; + } else { + ui.status.textContent = `Error in ${settings.mode} run: ${String(err)}`; + } + } finally { + if (state.activeRunId === runId) { + setRunningView(settings.mode, false); + setControlsDisabled(false); + state.isRunning = false; + } + } +} + +function runSearchChunk(settings: RunSettings, chunkIterations: number, startIndex: number): SearchChunkResult { + if (settings.mode === "parallel") { + return run_best_tour_par( + chunkIterations, + settings.seed, + settings.threads, + settings.numCities, + BigInt(startIndex) + ) as SearchChunkResult; + } + + return run_best_tour_seq( + chunkIterations, + settings.seed, + settings.numCities, + BigInt(startIndex) + ) as SearchChunkResult; +} + +function toAggregate(best: SearchChunkResult, iterations: number, elapsedMs: number): RunAggregate { + return { + best_tour: best.best_tour, + best_distance: best.best_distance, + iterations, + elapsed_ms: elapsedMs + }; +} + +function chooseChunkSize(mode: SearchMode, numCities: number, iterations: number) { + const base = mode === "parallel" ? 400 : 240; + const scale = Math.max(1, Math.floor(220 / Math.max(numCities, 5))); + return Math.max(8, Math.min(iterations, base * scale)); +} + +function setRunningView(mode: SearchMode, running: boolean) { + if (running) { + state.cancelRequested = false; + state.runStartedAtMs = performance.now(); + ui.runTitle.textContent = mode === "parallel" ? "Running parallel search..." : "Running sequential search..."; + ui.runSubtitle.textContent = "Evaluating tours with 2-opt local search. Larger instances can take several minutes."; + ui.runElapsed.textContent = "Elapsed: 0.0s"; + ui.cancelRun.disabled = false; + ui.runOverlay.classList.add("active"); + ui.runOverlay.setAttribute("aria-hidden", "false"); + + if (state.runTicker !== undefined) { + window.clearInterval(state.runTicker); + } + + state.runTicker = window.setInterval(() => { + const secs = (performance.now() - state.runStartedAtMs) / 1000; + ui.runElapsed.textContent = `Elapsed: ${secs.toFixed(1)}s`; + }, 200); + return; + } + + if (state.runTicker !== undefined) { + window.clearInterval(state.runTicker); + state.runTicker = undefined; + } + + ui.cancelRun.disabled = true; + ui.runOverlay.classList.remove("active"); + ui.runOverlay.setAttribute("aria-hidden", "true"); +} + +function nextPaint() { + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()); + }); +} + +async function allowRunningOverlayToRender() { + // Two frames + a tiny timeout makes spinner start reliably before sync wasm work blocks the UI thread. + await nextPaint(); + await nextPaint(); + await new Promise((resolve) => window.setTimeout(resolve, 24)); +} + +function readNumCities() { + const parsed = ui.numCities.valueAsNumber; + + if (!Number.isFinite(parsed)) { + ui.numCities.value = String(state.currentNumCities); + return state.currentNumCities; + } + + const numCities = Math.max(MIN_CITIES, Math.min(MAX_CITIES, Math.trunc(parsed))); + state.currentNumCities = numCities; + ui.numCities.value = String(numCities); + return numCities; +} + +function readThreads() { + const parsed = ui.threads.valueAsNumber; + + if (!Number.isFinite(parsed)) { + ui.threads.value = String(MIN_THREADS); + return MIN_THREADS; + } + + const threads = Math.max(MIN_THREADS, Math.min(MAX_THREADS, Math.trunc(parsed))); + ui.threads.value = String(threads); + return threads; +} + +function clearBest() { + state.bestSoFar = null; + ui.bestDistance.textContent = "-"; + ui.elapsed.textContent = "-"; + ui.ips.textContent = "-"; +} + +function updateStats(result: RunAggregate) { + ui.bestDistance.textContent = result.best_distance.toFixed(3); + ui.elapsed.textContent = `${result.elapsed_ms.toFixed(1)} ms`; + const ips = result.iterations / Math.max(result.elapsed_ms / 1000, 1e-9); + ui.ips.textContent = ips.toFixed(0); +} + +function drawPoints(locations: Location[]) { + ctx.clearRect(0, 0, ui.canvas.width, ui.canvas.height); + const mapped = mapPoints(locations); + + for (const p of mapped) { + ctx.beginPath(); + ctx.fillStyle = "#0f766e"; + ctx.arc(p.x, p.y, 5, 0, Math.PI * 2); + ctx.fill(); + } +} + +function drawTour(locations: Location[], tour: number[]) { + drawPoints(locations); + if (tour.length === 0) { + return; + } + + const mapped = mapPoints(locations); + ctx.beginPath(); + ctx.strokeStyle = "#f59e0b"; + ctx.lineWidth = 2; + + const first = mapped[tour[0]]; + ctx.moveTo(first.x, first.y); + + for (const idx of tour.slice(1)) { + const p = mapped[idx]; + ctx.lineTo(p.x, p.y); + } + + ctx.lineTo(first.x, first.y); + ctx.stroke(); +} + +function mapPoints(locations: Location[]) { + const pad = 28; + const xs = locations.map((p) => p.x); + const ys = locations.map((p) => p.y); + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); + const spanX = Math.max(maxX - minX, 1); + const spanY = Math.max(maxY - minY, 1); + + return locations.map((p) => ({ + x: pad + ((p.x - minX) / spanX) * (ui.canvas.width - pad * 2), + y: ui.canvas.height - (pad + ((p.y - minY) / spanY) * (ui.canvas.height - pad * 2)) + })); +} + +void setupApp(); diff --git a/examples/wasm_demo_tsp2/web/styles.css b/examples/wasm_demo_tsp2/web/styles.css new file mode 100644 index 00000000..003dc373 --- /dev/null +++ b/examples/wasm_demo_tsp2/web/styles.css @@ -0,0 +1,237 @@ +:root { + --bg: #f6f7fb; + --ink: #14213d; + --panel: #ffffff; + --accent: #0f766e; + --accent2: #f59e0b; +} + +body { + margin: 0; + background: radial-gradient(circle at 20% 0%, #eef2ff 0, var(--bg) 55%); + color: var(--ink); + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; +} + +main { + max-width: 980px; + margin: 32px auto; + padding: 20px; +} + +.card { + position: relative; + background: var(--panel); + border-radius: 16px; + box-shadow: 0 8px 24px rgba(20, 33, 61, 0.08); + padding: 16px; +} + +.run-overlay { + position: absolute; + inset: 16px; + display: none; + align-items: center; + justify-content: center; + border-radius: 12px; + background: rgba(15, 23, 42, 0.52); + backdrop-filter: blur(2px); + z-index: 5; +} + +.run-overlay.active { + display: flex; +} + +.run-overlay-card { + min-width: 260px; + max-width: 460px; + background: #ffffff; + border: 1px solid #dbe3ef; + border-radius: 14px; + padding: 14px 16px; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.14); +} + +.run-overlay-top { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.spinner { + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #d1d5db; + border-top-color: #0f766e; + animation: spin 0.9s linear infinite; +} + +.run-title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: #0f172a; +} + +.run-subtitle { + margin: 0; + font-size: 13px; + color: #334155; +} + +.run-elapsed { + margin: 8px 0 0; + font-size: 12px; + font-weight: 600; + color: #475569; +} + +.run-actions { + margin-top: 10px; + display: flex; + justify-content: flex-end; +} + +button.cancel { + background: linear-gradient(90deg, #b91c1c, #dc2626); +} + +.run-bar { + margin-top: 10px; + width: 100%; + height: 6px; + border-radius: 999px; + background: #e2e8f0; + overflow: hidden; +} + +.run-bar::after { + content: ""; + display: block; + width: 35%; + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, #0f766e, #0ea5a1); + animation: slide 1.4s ease-in-out infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes slide { + 0% { + transform: translateX(-120%); + } + + 50% { + transform: translateX(85%); + } + + 100% { + transform: translateX(260%); + } +} + +.controls { + display: grid; + grid-template-columns: repeat(4, minmax(120px, 1fr)); + gap: 12px; + margin-bottom: 14px; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 12px; +} + +label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; +} + +input { + border: 1px solid #d1d5db; + border-radius: 10px; + padding: 8px 10px; + font-size: 14px; +} + +button { + border: 0; + border-radius: 12px; + padding: 10px 12px; + background: linear-gradient(90deg, var(--accent), #0ea5a1); + color: #fff; + font-weight: 600; + cursor: pointer; +} + +button.secondary { + background: linear-gradient(90deg, #2563eb, #4f46e5); +} + +button.ghost { + background: #f3f4f6; + color: #1f2937; + border: 1px solid #d1d5db; +} + +#status { + font-size: 14px; + min-height: 20px; + margin-bottom: 10px; +} + +.stats { + display: grid; + grid-template-columns: repeat(3, minmax(120px, 1fr)); + gap: 10px; + margin-bottom: 10px; +} + +.stat { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 10px; + padding: 8px; +} + +.stat h3 { + margin: 0; + font-size: 12px; + color: #64748b; +} + +.stat p { + margin: 4px 0 0; + font-size: 16px; + font-weight: 600; +} + +canvas { + width: 100%; + height: 430px; + border: 1px solid #e5e7eb; + border-radius: 12px; + background: #fff; +} + +@media (max-width: 760px) { + .controls { + grid-template-columns: 1fr; + } + + .stats { + grid-template-columns: 1fr; + } +} diff --git a/examples/wasm_demo_tsp2/web/tsconfig.json b/examples/wasm_demo_tsp2/web/tsconfig.json new file mode 100644 index 00000000..d9e4b302 --- /dev/null +++ b/examples/wasm_demo_tsp2/web/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "isolatedModules": true, + "skipLibCheck": true, + "types": [ + "vite/client" + ] + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/examples/wasm_demo_tsp2/web/vite.config.ts b/examples/wasm_demo_tsp2/web/vite.config.ts new file mode 100644 index 00000000..ed8fe834 --- /dev/null +++ b/examples/wasm_demo_tsp2/web/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { + headers: { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp" + } + }, + worker: { + format: "es" + } +}); diff --git a/src/lib.rs b/src/lib.rs index 14166f68..daa20d84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,11 +46,35 @@ pub use option_use::ParUseOption; pub use parameters::{ChunkSize, IterationOrder, NumThreads, Params}; #[cfg(feature = "std")] pub use pool::BasicPool; -pub use pool::{ParThreadPool, Pool}; #[cfg(all(feature = "wasm-web-threads", target_arch = "wasm32"))] pub use pool::WasmWebPool; -#[cfg(all(feature = "wasm-web-threads", target_arch = "wasm32", target_feature = "atomics"))] +#[cfg(all(feature = "wasm-web-threads2", target_arch = "wasm32"))] +pub use pool::WasmWebPool2; +#[cfg(all( + feature = "wasm-web-threads", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use pool::init_thread_pool; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] pub use pool::init_thread_pool; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use pool::wasm_web2_runtime_info; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use pool::wasm_web2_start_worker; +pub use pool::{ParThreadPool, Pool}; pub use result::ParResult; pub use result_use::ParUseResult; pub use runner::Runner; diff --git a/src/pool/mod.rs b/src/pool/mod.rs index 01746ace..7b6b95ee 100644 --- a/src/pool/mod.rs +++ b/src/pool/mod.rs @@ -3,6 +3,15 @@ mod new_pool; mod par_thread_pool; mod pool_impl; +#[cfg(all( + feature = "wasm-web-threads", + feature = "wasm-web-threads2", + target_arch = "wasm32" +))] +compile_error!( + "Features 'wasm-web-threads' and 'wasm-web-threads2' are mutually exclusive on wasm32; enable only one wasm backend feature." +); + pub use new_pool::Pool; pub use par_thread_pool::ParThreadPool; @@ -10,18 +19,49 @@ pub use par_thread_pool::ParThreadPool; pub use pool_impl::BasicPool; #[cfg(all(feature = "wasm-web-threads", target_arch = "wasm32"))] pub use pool_impl::WasmWebPool; +#[cfg(all(feature = "wasm-web-threads2", target_arch = "wasm32"))] +#[allow(unused_imports)] +pub use pool_impl::WasmWebPool2; #[cfg(all( feature = "wasm-web-threads", target_arch = "wasm32", target_feature = "atomics" ))] pub use pool_impl::init_thread_pool; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use pool_impl::init_thread_pool; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use pool_impl::wasm_web2_runtime_info; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use pool_impl::wasm_web2_start_worker; -#[cfg(all(feature = "std", feature = "wasm-web-threads", target_arch = "wasm32"))] +#[cfg(all(feature = "std", feature = "wasm-web-threads2", target_arch = "wasm32"))] +pub type DefaultPool = pool_impl::WasmWebPool2; +#[cfg(all( + feature = "std", + feature = "wasm-web-threads", + target_arch = "wasm32", + not(feature = "wasm-web-threads2") +))] pub type DefaultPool = pool_impl::WasmWebPool; #[cfg(all( feature = "std", - not(all(feature = "wasm-web-threads", target_arch = "wasm32")) + not(any( + all(feature = "wasm-web-threads", target_arch = "wasm32"), + all(feature = "wasm-web-threads2", target_arch = "wasm32") + )) ))] pub type DefaultPool = pool_impl::OncePool; #[cfg(not(feature = "std"))] diff --git a/src/pool/new_pool.rs b/src/pool/new_pool.rs index 84d8879a..650e640f 100644 --- a/src/pool/new_pool.rs +++ b/src/pool/new_pool.rs @@ -2,6 +2,8 @@ use crate::NumThreads; #[cfg(all(feature = "wasm-web-threads", target_arch = "wasm32"))] use crate::pool::pool_impl::WasmWebPool; +#[cfg(all(feature = "wasm-web-threads2", target_arch = "wasm32"))] +use crate::pool::pool_impl::WasmWebPool2; #[cfg(feature = "std")] use crate::{BasicPool, pool::pool_impl::OncePool}; @@ -224,4 +226,22 @@ impl Pool { pub fn wasm_web(num_threads: impl Into) -> WasmWebPool { WasmWebPool::new(num_threads) } + + /// Creates a wasm web-thread pool adapter using the new internal backend. + /// + /// This pool is intended for `wasm32` web builds and does not depend on Rayon runtime. + /// + /// # Parameters + /// + /// - `num_threads` - Desired maximum number of threads for computations: + /// - `NumThreads::Auto` uses backend default supported thread count + /// - `NumThreads::Max(n)` caps usage at `n` + /// + /// # Features + /// + /// Requires `wasm-web-threads2` feature and `wasm32` target. + #[cfg(all(feature = "wasm-web-threads2", target_arch = "wasm32"))] + pub fn wasm_web2(num_threads: impl Into) -> WasmWebPool2 { + WasmWebPool2::new(num_threads) + } } diff --git a/src/pool/pool_impl/mod.rs b/src/pool/pool_impl/mod.rs index 3a0bcd9b..16627d51 100644 --- a/src/pool/pool_impl/mod.rs +++ b/src/pool/pool_impl/mod.rs @@ -23,6 +23,29 @@ pub use wasm_web::WasmWebPool; ))] pub use wasm_web::init_thread_pool; +#[cfg(all(feature = "wasm-web-threads2", target_arch = "wasm32"))] +mod wasm_web2; +#[cfg(all(feature = "wasm-web-threads2", target_arch = "wasm32"))] +pub use wasm_web2::WasmWebPool2; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use wasm_web2::init_thread_pool; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use wasm_web2::wasm_web2_runtime_info; +#[cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] +pub use wasm_web2::wasm_web2_start_worker; + #[cfg(feature = "std")] mod basic; #[cfg(feature = "std")] diff --git a/src/pool/pool_impl/wasm_web2.rs b/src/pool/pool_impl/wasm_web2.rs new file mode 100644 index 00000000..897dd5bd --- /dev/null +++ b/src/pool/pool_impl/wasm_web2.rs @@ -0,0 +1,467 @@ +use crate::NumThreads; +use crate::pool::ParThreadPool; +use crate::pool::env::max_num_threads_by_env_and_resource; +use alloc::format; +use core::num::NonZeroUsize; +use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; +use js_sys::Promise; +use std::any::Any; +use std::boxed::Box; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; +use std::sync::{Arc, Condvar, Mutex, OnceLock}; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +const WASM_WEB2_THREAD_POOL_UNINITIALIZED: u8 = 0; +const WASM_WEB2_THREAD_POOL_INITIALIZED: u8 = 1; + +#[cfg_attr(not(target_feature = "atomics"), allow(dead_code))] +static WASM_WEB2_THREAD_POOL_STATE: AtomicU8 = AtomicU8::new(WASM_WEB2_THREAD_POOL_UNINITIALIZED); +#[cfg_attr(not(target_feature = "atomics"), allow(dead_code))] +static WASM_WEB2_THREAD_POOL_NUM_THREADS: AtomicUsize = AtomicUsize::new(0); +static WASM_WEB2_RUNTIME: OnceLock> = OnceLock::new(); + +#[wasm_bindgen(module = "/src/pool/pool_impl/worker_helpers2.js")] +extern "C" { + #[wasm_bindgen(js_name = startWorkers)] + fn start_workers(module: JsValue, memory: JsValue, num_threads: usize) -> Promise; +} + +struct Inner { + shared: Arc, + spawned_workers: usize, +} + +struct WorkerShared { + state: Mutex, + cv: Condvar, +} + +struct WorkerState { + shutdown: bool, + active_scope_addr: Option, + queue: VecDeque, +} + +impl Drop for Inner { + fn drop(&mut self) { + { + let mut state = self.shared.state.lock().expect("poisoned pool lock"); + state.shutdown = true; + while let Some(task) = state.queue.pop_front() { + unsafe { task.drop() }; + } + } + self.shared.cv.notify_all(); + } +} + +struct ScopeRuntime { + pending: AtomicUsize, + completion_lock: Mutex<()>, + completion_cv: Condvar, + panic: Mutex>>, +} + +#[cfg_attr(not(target_feature = "atomics"), allow(dead_code))] +impl ScopeRuntime { + fn new() -> Self { + Self { + pending: AtomicUsize::new(0), + completion_lock: Mutex::new(()), + completion_cv: Condvar::new(), + panic: Mutex::new(None), + } + } + + fn begin_task(&self) { + self.pending.fetch_add(1, Ordering::AcqRel); + } + + fn complete_task(&self) { + if self.pending.fetch_sub(1, Ordering::AcqRel) == 1 { + let _guard = self + .completion_lock + .lock() + .expect("poisoned scope completion lock"); + self.completion_cv.notify_all(); + } + } + + fn record_panic(&self, err: Box) { + let mut panic_slot = self.panic.lock().expect("poisoned scope panic lock"); + if panic_slot.is_none() { + *panic_slot = Some(err); + } + } + + fn take_panic(&self) -> Option> { + self.panic.lock().expect("poisoned scope panic lock").take() + } +} + +pub struct ScopeRef<'env> { + shared: *const WorkerShared, + runtime: *const ScopeRuntime, + inline_only: bool, + _marker: PhantomData<&'env ()>, +} + +impl<'env> ScopeRef<'env> { + fn shared(&self) -> &WorkerShared { + unsafe { &*self.shared } + } + + fn runtime(&self) -> &ScopeRuntime { + unsafe { &*self.runtime } + } +} + +#[cfg_attr(not(target_feature = "atomics"), allow(dead_code))] +struct Task { + data: *mut (), + run_fn: unsafe fn(*mut ()), + drop_fn: unsafe fn(*mut ()), +} + +unsafe impl Send for Task {} + +#[cfg_attr(not(target_feature = "atomics"), allow(dead_code))] +impl Task { + fn new(work: W) -> Self + where + W: Fn() + Send, + { + unsafe fn run_impl(data: *mut ()) + where + W: Fn() + Send, + { + let work = unsafe { Box::from_raw(data as *mut W) }; + (*work)(); + } + + unsafe fn drop_impl(data: *mut ()) + where + W: Fn() + Send, + { + drop(unsafe { Box::from_raw(data as *mut W) }); + } + + let boxed = Box::new(work); + Self { + data: Box::into_raw(boxed) as *mut (), + run_fn: run_impl::, + drop_fn: drop_impl::, + } + } + + unsafe fn run(self) { + unsafe { (self.run_fn)(self.data) }; + } + + unsafe fn drop(self) { + unsafe { (self.drop_fn)(self.data) }; + } +} + +#[cfg_attr(not(target_feature = "atomics"), allow(dead_code))] +fn worker_loop(shared: Arc) { + loop { + let (task, runtime_ptr) = { + let mut state = shared.state.lock().expect("poisoned pool lock"); + loop { + if state.shutdown { + return; + } + + if let Some(task) = state.queue.pop_front() { + let runtime_ptr = state + .active_scope_addr + .expect("active scope must be set while queue is non-empty"); + break (task, runtime_ptr as *const ScopeRuntime); + } + + state = shared.cv.wait(state).expect("poisoned pool lock"); + } + }; + + let runtime = unsafe { &*runtime_ptr }; + let result = catch_unwind(AssertUnwindSafe(|| unsafe { task.run() })); + if let Err(err) = result { + runtime.record_panic(err); + } + runtime.complete_task(); + } +} + +#[cfg_attr(not(target_feature = "atomics"), allow(dead_code))] +fn init_runtime(num_threads: NonZeroUsize) -> Arc { + let shared = Arc::new(WorkerShared { + state: Mutex::new(WorkerState { + shutdown: false, + active_scope_addr: None, + queue: VecDeque::new(), + }), + cv: Condvar::new(), + }); + + Arc::new(Inner { + shared, + spawned_workers: num_threads.get(), + }) +} + +/// Initializes the worker-backed wasm thread runtime for `WasmWebPool2`. +/// +/// This establishes the runtime init contract for the new wasm backend. +#[cfg(target_feature = "atomics")] +pub fn init_thread_pool(num_threads: usize) -> js_sys::Promise { + let num_threads = NonZeroUsize::new(num_threads.max(1)).expect(">0"); + + match WASM_WEB2_THREAD_POOL_STATE.compare_exchange( + WASM_WEB2_THREAD_POOL_UNINITIALIZED, + WASM_WEB2_THREAD_POOL_INITIALIZED, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => { + WASM_WEB2_THREAD_POOL_NUM_THREADS.store(num_threads.get(), Ordering::SeqCst); + + let _ = WASM_WEB2_RUNTIME.get_or_init(|| init_runtime(num_threads)); + + start_workers( + wasm_bindgen::module(), + wasm_bindgen::memory(), + num_threads.get(), + ) + } + Err(WASM_WEB2_THREAD_POOL_INITIALIZED) => { + let configured_threads = WASM_WEB2_THREAD_POOL_NUM_THREADS.load(Ordering::SeqCst); + + match configured_threads == num_threads.get() { + true => js_sys::Promise::resolve(&wasm_bindgen::JsValue::UNDEFINED), + false => js_sys::Promise::reject(&wasm_bindgen::JsValue::from_str(&format!( + "init_thread_pool was already called with {configured_threads} threads; refusing to reinitialize with {} threads", + num_threads.get() + ))), + } + } + Err(_) => unreachable!("invalid wasm-web-threads2 init state"), + } +} + +#[cfg(target_feature = "atomics")] +/// Returns `(configured_threads, spawned_workers)` for the active wasm-web-threads2 runtime. +pub fn wasm_web2_runtime_info() -> (usize, usize) { + let configured_threads = WASM_WEB2_THREAD_POOL_NUM_THREADS.load(Ordering::SeqCst); + let spawned_workers = runtime().spawned_workers; + (configured_threads, spawned_workers) +} + +#[cfg(target_feature = "atomics")] +#[wasm_bindgen] +/// Worker entrypoint called from `worker_helpers2.js` after wasm init. +pub fn wasm_web2_start_worker() { + let shared = Arc::clone(&runtime().shared); + worker_loop(shared); +} + +fn assert_wasm_thread_pool_initialized() { + assert!( + cfg!(target_feature = "atomics"), + "Wasm web threading requires atomics-enabled wasm build flags; see docs/wasm-plan-b.md." + ); + + assert_eq!( + WASM_WEB2_THREAD_POOL_STATE.load(Ordering::SeqCst), + WASM_WEB2_THREAD_POOL_INITIALIZED, + "Wasm web2 thread pool is not initialized. Call and await init_thread_pool(...) before running parallel computations." + ) +} + +fn runtime() -> &'static Arc { + assert_wasm_thread_pool_initialized(); + WASM_WEB2_RUNTIME.get_or_init(|| { + let num_threads = WASM_WEB2_THREAD_POOL_NUM_THREADS.load(Ordering::SeqCst); + let num_threads = NonZeroUsize::new(num_threads) + .expect("wasm web2 configured thread count must be > 0 after init_thread_pool"); + init_runtime(num_threads) + }) +} + +/// wasm web-thread pool adapter for the new internal backend. +#[derive(Clone, Copy, Debug)] +pub struct WasmWebPool2 { + max_num_threads: NonZeroUsize, +} + +impl Default for WasmWebPool2 { + fn default() -> Self { + Self::new(NumThreads::Auto) + } +} + +impl WasmWebPool2 { + /// Creates a new wasm web-thread pool adapter. + pub fn new(num_threads: impl Into) -> Self { + let max_num_threads = match num_threads.into() { + NumThreads::Auto => max_num_threads_by_env_and_resource(), + NumThreads::Max(n) => max_num_threads_by_env_and_resource().min(n), + }; + + Self { max_num_threads } + } + + fn scoped_computation_impl<'env, 'scope, F>(&'env self, f: F) + where + 'env: 'scope, + for<'s> F: FnOnce(&'s ScopeRef<'env>) + Send, + { + let scope_runtime = ScopeRuntime::new(); + + { + let runtime_ref = runtime(); + let mut state = runtime_ref.shared.state.lock().expect("poisoned pool lock"); + debug_assert!(state.active_scope_addr.is_none()); + state.active_scope_addr = Some(&scope_runtime as *const ScopeRuntime as usize); + } + + let scope_ref = ScopeRef { + shared: Arc::as_ptr(&runtime().shared), + runtime: &scope_runtime, + inline_only: runtime().spawned_workers == 0, + _marker: PhantomData, + }; + + let user_result = catch_unwind(AssertUnwindSafe(|| f(&scope_ref))); + + #[cfg(target_arch = "wasm32")] + { + // Main-thread wasm cannot block on Condvar/Atomics.wait. + while scope_runtime.pending.load(Ordering::Acquire) != 0 { + let maybe_task = { + let runtime_ref = runtime(); + let mut state = runtime_ref.shared.state.lock().expect("poisoned pool lock"); + state.queue.pop_front() + }; + + if let Some(task) = maybe_task { + let result = catch_unwind(AssertUnwindSafe(|| unsafe { task.run() })); + if let Err(err) = result { + scope_runtime.record_panic(err); + } + scope_runtime.complete_task(); + } else { + core::hint::spin_loop(); + } + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + let mut guard = scope_runtime + .completion_lock + .lock() + .expect("poisoned scope completion lock"); + while scope_runtime.pending.load(Ordering::Acquire) != 0 { + guard = scope_runtime + .completion_cv + .wait(guard) + .expect("poisoned scope completion lock"); + } + } + + { + let runtime_ref = runtime(); + let mut state = runtime_ref.shared.state.lock().expect("poisoned pool lock"); + state.active_scope_addr = None; + debug_assert!(state.queue.is_empty()); + } + + if let Err(err) = user_result { + resume_unwind(err); + } + + if let Some(err) = scope_runtime.take_panic() { + resume_unwind(err); + } + } +} + +impl ParThreadPool for WasmWebPool2 { + type ScopeRef<'s, 'env, 'scope> + = &'s ScopeRef<'env> + where + 'scope: 's, + 'env: 'scope + 's; + + fn run_in_scope<'s, 'env, 'scope, W>(s: &Self::ScopeRef<'s, 'env, 'scope>, work: W) + where + 'scope: 's, + 'env: 'scope + 's, + W: Fn() + Send + 'scope + 'env, + { + s.runtime().begin_task(); + + if s.inline_only { + let result = catch_unwind(AssertUnwindSafe(work)); + if let Err(err) = result { + s.runtime().record_panic(err); + } + s.runtime().complete_task(); + return; + } + + let task = Task::new(work); + + { + let mut state = s.shared().state.lock().expect("poisoned pool lock"); + state.queue.push_back(task); + } + + s.shared().cv.notify_one(); + } + + fn scoped_computation<'env, 'scope, F>(&'env mut self, f: F) + where + 'env: 'scope, + for<'s> F: FnOnce(&'s ScopeRef<'env>) + Send, + { + self.scoped_computation_impl(f) + } + + fn max_num_threads(&self) -> NonZeroUsize { + self.max_num_threads + } +} + +impl ParThreadPool for &WasmWebPool2 { + type ScopeRef<'s, 'env, 'scope> + = &'s ScopeRef<'env> + where + 'scope: 's, + 'env: 'scope + 's; + + fn run_in_scope<'s, 'env, 'scope, W>(s: &Self::ScopeRef<'s, 'env, 'scope>, work: W) + where + 'scope: 's, + 'env: 'scope + 's, + W: Fn() + Send + 'scope + 'env, + { + ::run_in_scope(s, work) + } + + fn scoped_computation<'env, 'scope, F>(&'env mut self, f: F) + where + 'env: 'scope, + for<'s> F: FnOnce(&'s ScopeRef<'env>) + Send, + { + (*self).scoped_computation_impl(f) + } + + fn max_num_threads(&self) -> NonZeroUsize { + self.max_num_threads + } +} diff --git a/src/pool/pool_impl/worker_helpers2.js b/src/pool/pool_impl/worker_helpers2.js new file mode 100644 index 00000000..1d36e36f --- /dev/null +++ b/src/pool/pool_impl/worker_helpers2.js @@ -0,0 +1,63 @@ +function waitForMsgType(target, type) { + return new Promise((resolve) => { + target.addEventListener("message", function onMsg({ data }) { + if (data == null || data.type !== type) return; + target.removeEventListener("message", onMsg); + resolve(data); + }); + }); +} + +waitForMsgType(self, "orx_parallel_worker_init").then(async ({ init }) => { + try { + const pkg = await import("../../../../.."); + await pkg.default(init); + postMessage({ type: "orx_parallel_worker_ready" }); + pkg.wasm_web2_start_worker(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + postMessage({ type: "orx_parallel_worker_error", message }); + } +}); + +let _workers; + +export async function startWorkers(module, memory, numThreads) { + if (numThreads === 0) { + throw new Error("num_threads must be > 0."); + } + + const workerInit = { + type: "orx_parallel_worker_init", + init: { module_or_path: module, memory }, + }; + + _workers = await Promise.all( + Array.from({ length: numThreads }, async (_, idx) => { + const worker = new Worker(new URL("./worker_helpers2.js", import.meta.url), { + type: "module", + }); + + const onWorkerError = new Promise((_, reject) => { + worker.addEventListener("error", (event) => { + reject(new Error(`worker ${idx} script error: ${event.message || "unknown error"}`)); + }, { once: true }); + }); + + const onWorkerInitError = waitForMsgType(worker, "orx_parallel_worker_error").then((data) => { + throw new Error(`worker ${idx} init failed: ${data.message || "unknown error"}`); + }); + + const onWorkerReady = waitForMsgType(worker, "orx_parallel_worker_ready"); + + const onWorkerTimeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`worker ${idx} init timed out`)), 10000); + }); + + worker.postMessage(workerInit); + + await Promise.race([onWorkerReady, onWorkerInitError, onWorkerError, onWorkerTimeout]); + return worker; + }) + ); +} diff --git a/tests/wasm_web_threads2_smoke.rs b/tests/wasm_web_threads2_smoke.rs new file mode 100644 index 00000000..02ccd2fc --- /dev/null +++ b/tests/wasm_web_threads2_smoke.rs @@ -0,0 +1,47 @@ +#![cfg(all( + feature = "wasm-web-threads2", + target_arch = "wasm32", + target_feature = "atomics" +))] + +use orx_parallel::init_thread_pool; +use orx_parallel::{IntoParIter, Par}; +use wasm_bindgen_futures::JsFuture; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test(async)] +async fn wasm_web2_init_is_idempotent_and_rejects_mismatch() { + JsFuture::from(init_thread_pool(2)) + .await + .expect("first init_thread_pool call should resolve"); + + JsFuture::from(init_thread_pool(2)) + .await + .expect("second init_thread_pool call with the same configuration should resolve"); + + let err = JsFuture::from(init_thread_pool(3)) + .await + .expect_err("reinitialization with a different thread count should be rejected"); + + let message = err + .as_string() + .expect("rejection reason should be a string message"); + + assert!( + message.contains("already called with 2 threads"), + "unexpected rejection message: {message}" + ); +} + +#[wasm_bindgen_test(async)] +async fn wasm_web2_default_pool_runs_without_explicit_pool() { + JsFuture::from(init_thread_pool(2)) + .await + .expect("init_thread_pool should resolve for the default-path smoke test"); + + let sum: usize = (0..128).into_par().sum(); + + assert_eq!(sum, (0..128usize).sum::()); +}