# `jsmpi` 开发指导(第一阶段)
## 1. 文档目标
本文档用于指导 `jsmpi` 的第一阶段开发,确保实现过程围绕以下核心原则展开:
- **尽量兼容 `rsmpi` API**
- **入口改动最小化**
- **先打通浏览器多 worker 运行闭环,再扩展功能**
- **先验证真实行为,再扩展抽象与优化**
---
## 2. 第一阶段工作范围
第一阶段只做“最小可运行闭环”,不要一开始追求完整 MPI 功能。
### 必做范围
- 创建 `jsmpi` crate 基础结构
- 建立 `wasm-bindgen` 浏览器入口
- 建立 `Coordinator + Worker + WASM` 启动链路
- 提供:
- `initialize()`
- `Universe::world()`
- `world.rank()`
- `world.size()`
- `process_at_rank(rank).send()`
- `any_process().receive()`
- `barrier()`
- 提供一个可运行示例:`ping_pong` / `hello_ranks`
### 暂缓范围
- 复杂 datatype 系统
- 非阻塞通信请求系统
- communicator split / cartesian topology
- 高级 reduce op / 自定义 operator
- 高性能优化
---
## 3. 推荐开发顺序
## Step 1:初始化项目骨架
建议先建立如下内容:
```text
jsmpi/
├─ Cargo.toml
├─ src/
│ ├─ lib.rs
│ ├─ environment.rs
│ ├─ topology.rs
│ ├─ runtime/
│ │ ├─ mod.rs
│ │ ├─ browser.rs
│ │ └─ protocol.rs
├─ js/
│ ├─ bootstrap.js
│ ├─ coordinator.js
│ └─ worker.js
└─ examples/
```
建议先让以下链路跑通:
1. 主线程打开页面
2. JS 创建 N 个 worker
3. 每个 worker 加载同一份 wasm 模块
4. worker 把 `rank` / `size` 注入到 Rust runtime
5. Rust 代码可读取 world 信息并输出日志
**验收标准:** 页面上能看到每个 rank 正常启动并输出自己的编号。
---
## Step 2:先做 runtime,再做 API 外观
开发顺序应为:
1. **先实现底层 runtime 通信协议**
2. **再封装 `rsmpi` 风格 API**
不要一开始就陷入 trait 细节。先确认以下事实成立:
- rank 间消息能送达
- Coordinator 能正确路由消息
- Worker 能等待消息并恢复执行
- 多个 rank 能在 barrier 汇合
**原则:真实行为优先于表面接口。**
---
## Step 3:实现最小兼容 API
第一批 API 应只覆盖用户最常写的路径:
```rust
use mpi::traits::*;
let universe = mpi::initialize().unwrap();
let world = universe.world();
let rank = world.rank();
let size = world.size();
world.barrier();
```
### 建议做法
- 模块名、类型名、方法名尽量贴近 `rsmpi`
- 暂未支持的接口先不暴露,或明确返回可理解错误
- 不要为了“名字兼容”而造出错误语义
---
## 4. 入口改造建议
用户现有 `rsmpi` 程序通常是:
```rust
fn main() {
run();
}
```
迁移到 `jsmpi` 时,建议调整为:
```rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn start() {
run();
}
```
或者:
```rust
#[wasm_bindgen]
pub fn jsmpi_main() {
run();
}
```
然后把原来的核心逻辑保留在:
```rust
fn run() {
let universe = mpi::initialize().unwrap();
let world = universe.world();
// 原有 MPI 逻辑基本保持不变
}
```
> 推荐规则:**把入口层改造限制在 10 行以内**,其余计算与通信逻辑保持原样。
---
## 5. 运行时实现建议
## 5.1 首版使用 Coordinator 中转
虽然 worker-to-worker 直连更快,但首版应由主线程统一中转。
优点:
- 调试简单
- 消息可观测
- barrier / broadcast / gather 更容易实现
- 容易保证正确性
### 推荐消息流
```text
Rank A worker
-> postMessage(send)
Coordinator
-> 查找目标 rank
-> 投递给 Rank B worker
Rank B worker
-> 匹配 receive 请求
```
---
## 5.2 阻塞语义实现建议
MPI 用户习惯的是阻塞式接口,但浏览器环境本质上是事件驱动的。
因此建议:
- 对外保留阻塞式/同步式 API 外观
- 内部使用消息队列 + 状态机 + Promise/回调桥接
- 在 Rust 层谨慎封装同步外观,避免卡死 UI 主线程
如果首版难以实现完全同步语义,可采用:
- worker 内部允许 async runtime 辅助
- 文档中说明浏览器环境下的语义差异
当前仓库里的建议做法是:
- 原生目标继续使用同步 `receive()` / `barrier()`
- `wasm32` 目标优先复用 `Runtime::receive_async()` 或 `Runtime::receive_with_timeout_async()`
- `wasm32` 目标在需要同步点时优先复用 `Runtime::barrier_async()`
- 示例层只保留通信意图,不重复实现轮询与超时控制
---
## 6. 代码风格约束
### 6.1 基本原则
- 保持模块职责单一
- 运行时协议与公共 API 分离
- 所有公共类型必须写文档注释
- 错误类型统一映射到 `jsmpi::Error`
### 6.2 命名建议
- 面向用户的命名:尽量对齐 `rsmpi`
- 内部 runtime 命名:清晰表达浏览器语义,如 `CoordinatorBridge`、`PendingReceive`
### 6.3 不建议做的事
- 不要把 JS 互操作逻辑散落在所有模块中
- 不要过早抽象复杂 trait 层级
- 不要在未验证实际通信前写大段兼容包装代码
---
## 7. 测试与验证策略
开发时必须遵循以下验证顺序:
## 7.1 先做最小真实示例
每实现一个阶段,都要优先验证真实浏览器行为。
推荐测试顺序:
1. 单 rank 启动
2. 双 rank 启动
3. rank0 -> rank1 单次发送
4. rank1 正确接收
5. 所有 rank barrier 汇合
## 7.2 示例优先于 mock
不要用大量 mock 证明消息能通信,应优先运行真实浏览器示例。
推荐示例:
- `examples/hello_ranks.rs`
- `examples/ping_pong.rs`
- `examples/broadcast_sum.rs`
---
## 8. P0 完成验收指令(当前基线)
在进入 P1 前,至少完成以下本地验收:
```bash
cargo check --lib
cargo test --lib
cargo check --examples --target wasm32-unknown-unknown
cd demo && trunk build
cd .. && npm run test:browser:e2e
```
说明:
- `npm run test:browser:e2e` 当前会执行 3 轮完整浏览器示例套件。
- 若 `wasm-bindgen` schema 报错,请对齐 `wasm-bindgen-cli` 版本到 `0.2.117`。
**判断标准:** 示例真实运行成功,优先级高于单纯单元测试通过。
---
## 8.1 P1 超时与重试策略基线
当前已在 `Runtime` 层提供可配置重试策略(`RetryPolicy`),覆盖:
- `send_with_retry_timeout(...)`
- `receive_with_retry_timeout(...)`
- `barrier_with_retry_timeout(...)`
策略参数:
- `max_retries`
- `initial_backoff`
- `max_backoff`
- `backoff_multiplier`
退化行为验证(当前最小基线):
```bash
cargo test --lib runtime::state::tests::receive_with_retry_timeout_stops_after_budget
cargo test --lib runtime::state::tests::retry_policy_backoff_is_capped
```
验证目标:
- 在持续超时场景下,重试次数受预算上限约束。
- 退避时间按策略增长但受 `max_backoff` 限制,不出现无界增长。
---
## 8. 第一阶段的依赖建议
`Cargo.toml` 初始可考虑:
```toml
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
console_error_panic_hook = "0.1"
```
如需浏览器 API:
```toml
web-sys = { version = "0.3", features = [
"Worker",
"MessageEvent",
"DedicatedWorkerGlobalScope",
"console",
] }
```
> `rsmpi` 本身依赖原生 MPI 环境,`jsmpi` 不应直接照搬其底层实现,而应主要复刻其用户层 API 体验。
---
## 9. 示例迁移策略
为了兑现“几乎不改业务代码”的目标,建议建立一个迁移模板。
### 原始 `rsmpi` 示例
```rust
fn main() {
let universe = mpi::initialize().unwrap();
let world = universe.world();
// ...
}
```
### 迁移后 `jsmpi` 示例
```rust
use wasm_bindgen::prelude::*;
use mpi::traits::*;
#[wasm_bindgen(start)]
pub fn start() {
let universe = mpi::initialize().unwrap();
let world = universe.world();
// ... 原逻辑尽量不变
}
```
迁移检查表:
- [ ] `use mpi::traits::*` 不变
- [ ] `initialize/world/rank/size` 不变
- [ ] `send/receive/barrier` 调用不变
- [ ] 只修改入口层
---
## 10. 第一阶段交付定义
当满足以下条件时,可认为第一阶段完成:
1. `jsmpi` crate 能成功编译为 `wasm`。
2. 浏览器可创建多个 worker 并运行同一 Rust 程序。
3. 应用代码可通过 `initialize().world()` 获取 rank/size。
4. 至少支持一次真实的 `send/receive`。
5. 至少支持一次真实的 `barrier`。
6. 有一个文档化 demo 展示“从 rsmpi 风格代码迁移到浏览器”的最小改动方案。
---
## 11. 下一步实际任务清单
建议马上进入如下执行项:
1. 创建 `Cargo.toml` 和 `src/lib.rs`
2. 先实现浏览器启动最小骨架
3. 增加 rank/size 注入与读取
4. 打通 `send/receive` 消息协议
5. 编写 `hello_ranks` 与 `ping_pong` 示例
6. 再回头补齐更广的 `rsmpi` API 包装
> 开发节奏建议:**每完成一个 API,就用真实多 worker 示例验证一次**,避免后期集中排错。