diff --git a/20260815-tasks-g3.md b/20260815-tasks-g3.md new file mode 100644 index 0000000..adad853 --- /dev/null +++ b/20260815-tasks-g3.md @@ -0,0 +1,96 @@ +# 20260815 — KẾ HOẠCH THỰC HIỆN G3 (HEALTH / WATCHDOG THÔNG MINH) + +Nguồn: `big_tasks.md` — header **G3 — Health / Watchdog thông minh (1-2 tuần)**. +Trạng thái roadmap: G0 ✅, G1 ✅, G2 ✅ (G2.0–G2.6). Branch: `standalone-shm-bridge`. + +--- + +## 1. PHÂN TÍCH G3 + +### 1.1 Mục tiêu +Watchdog thông minh, cô lập lỗi: +- G3.1 heartbeat riêng: heartbeat bridge tách khỏi logic audio. +- G3.2 phân loại stall: phân biệt hang/crash/stall để xử lý đúng mức. +- G3.3 SEH quanh process từng channel: 1 plugin crash → chỉ mute channel đó, không sập cả bridge. + +### 1.2 Hiện trạng (đã verify trong code) +| Thành phần | File | Hiện trạng liên quan G3 | +|---|---|---| +| SHM layout | `native_bridge/include/SharedMemoryIPC.h` + `src-tauri/src/shm.rs` (phải khớp, assert size) | `bridgeWriteIndex` @4 (mỗi block render), `attachInFlight` @11160 (G0.3). Tổng 11164B. Không có heartbeat riêng. | +| Watchdog Rust | `src-tauri/src/lib.rs` pump thread | Liveness = `write_index()` đổi trong 3s; nếu `attach_in_flight` → deadline 15s; stall → kill + spawn lại 1 lần rồi `bridge-down`. | +| Bridge C++ | `native_bridge/src/main.cpp` | Main loop = audio loop (render + drain control); mọi attach/load chạy trên UiThread (`uiWorker.post`); heartbeat không tồn tại. | + +### 1.3 Khoảng trống (gap) +1. **Liveness gắn với audio loop** — writeIndex chỉ tăng khi audio loop còn render; attach job dài (Nexus >3s, G0.3) làm audio loop nghẽn → watchdog tưởng bridge chết → phải kéo deadline lên 15s (đánh đổi: bridge thật chết cũng chờ 15s). +2. **Không phân loại được stall** — hang (audio loop kẹt, process còn sống) vs crash (process chết) xử lý giống nhau. +3. **1 plugin chết kéo cả bridge** — chưa có SEH/process con từng channel (G3.3). + +### 1.4 Nguyên tắc thiết kế (đề xuất) +- **Heartbeat là nguồn liveness duy nhất** (G3.1): bridge chạy thread riêng tăng `heartbeat` ~10Hz trong SHM, độc lập audio loop → attach dài không còn ảnh hưởng deadline; bỏ ngoại lệ `attach_in_flight` trong watchdog. +- **Audio stall ≠ bridge chết** (G3.1 lót đường G3.2): heartbeat còn mà writeIndex đứng → log "audio stall (heartbeat OK)" — không restart. +- **Field append-only** — giữ offset cũ (như G0.3): thêm `heartbeat` sau `attachInFlight` @11164. +- **Không đụng audio loop** — heartbeat thread riêng, Sleep(100ms), không lock. + +--- + +## 2. PHÂN RÃ TASKS + ESTIMATE + +| ID | Task | File đích | Nội dung | Estimate | +|---|---|---|---|---| +| G3.1 | Heartbeat riêng | `SharedMemoryIPC.h`, `shm.rs`, `main.cpp`, `lib.rs`, `shm_selfcheck.cpp` | SHM thêm `heartbeat:u32` (append @11164, size 11168); bridge thread riêng tăng 100ms; Rust pump dùng heartbeat cho liveness (deadline 3s, bỏ ngoại lệ attach); writeIndex stall khi heartbeat còn → log warning, không restart. | 1 ngày | +| G3.2 | Phân loại stall | `lib.rs`, `main.cpp` | hang vs crash vs stall; xử lý đúng mức (restart / mute / chỉ log). | 2 ngày | +| G3.3 | SEH từng channel | `main.cpp` | 1 plugin crash → chỉ mute channel, không sập bridge. | 3 ngày | +| | **Tổng** | | | **6 ngày (~1.5 tuần — khớp roadmap)** | + +### Phụ thuộc +`G3.2 → G3.1` (cần heartbeat) · `G3.3 → G3.2` (cần phân loại). + +### Thứ tự triển khai khuyến nghị +G3.1 → G3.2 → G3.3. + +--- + +## 3. KẾ HOẠCH THỰC HIỆN (CÀI ĐẶT → KIỂM TRA → BẮT BUGS → FIX → REPORT) + +### G3.1 — Heartbeat riêng +- **Cài đặt**: + - `SharedMemoryIPC.h`: thêm `volatile uint32_t heartbeat;` sau `attachInFlight` (append-only, offset 11164). + - `shm.rs`: thêm field `heartbeat: u32` + accessor `heartbeat()` + sửa assert size 11168. + - `main.cpp`: trước main loop, spawn thread `[shmIPC]{ for(;;){ Sleep(100); shmIPC->heartbeat++; } }` detach. + - `lib.rs` pump: liveness = heartbeat (đổi trong 3s → restart 1 lần → `bridge-down`); writeIndex chỉ dùng emit audio; heartbeat còn + writeIndex đứng ≥3s → log `[tauri] audio stall (heartbeat OK)`, không restart. Bỏ `attach_in_flight` khỏi deadline. + - `shm_selfcheck.cpp`: assert size 11168. +- **Kiểm tra**: build bridge (build_bridge.ps1) + `cargo build --release` (src-tauri) → copy exe vào install/ → deploy → probe SHM: heartbeat tăng ~10Hz; kill bridge → heartbeat đứng → watchdog restart (log) → heartbeat tăng lại; regression G1.5 subset + G2.5 stress vẫn PASS. +- **Bắt bugs**: attach dài không còn risk restart (bỏ ngoại lệ 15s); heartbeat sai offset (assert size bắt). +- **Fix + report**: mục 5. +- **📌 Lưu ý & quy trình**: + - **Windows** (sửa code): patch bytes cho file CRLF (SharedMemoryIPC.h, main.cpp), text thường cho lib.rs (LF). Build bridge + cargo release. Deploy: kill 3 proc → copy `target/release/sonicforge-daw.exe` + `native_bridge/build/Release/daw_vst_bridge.exe` vào `install/` → start → PIDs mới. Chạy probe heartbeat + stress. Commit: `G3.1: dedicated bridge heartbeat — watchdog liveness independent of audio loop`. + - **Linux** (ghi chú): pull + review; không chạy được (cần SHM Windows); cập nhật mục 5. + +--- + +## 4. RỦI RO +| Rủi ro | Ảnh hưởng | Giảm thiểu | +|---|---|---| +| Heartbeat thread bị kill cùng process | Vẫn restart như cũ (không tệ hơn) | OK — heartbeat chết = process chết = restart đúng. | +| Heartbeat sai offset → đè field khác | Hỏng SHM | Assert size 11168 ở cả 2 phía (shm.rs test + shm_selfcheck.cpp). | +| Bỏ ngoại lệ attach 15s → attach dài bị restart | Mất GUI giữa attach | Heartbeat thread độc lập → attach không làm heartbeat đứng; chỉ restart khi process thật chết. | +| Audio loop kẹt vĩnh viễn (plugin hang) nhưng heartbeat còn | Không restart → câm vĩnh viễn | G3.2 phân loại + xử lý (log cảnh báo ở G3.1, có dấu hiệu trong log). | + +--- + +## 5. BÁO CÁO KẾT QUẢ (điền sau khi thực hiện) +| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái | +|---|---|---|---|---|---| +| G3.1 | Heartbeat @11164 (size 11168), thread riêng Sleep(100) tăng `heartbeat`; pump liveness = heartbeat (deadline 3s, restart 1 lần); bỏ ngoại lệ attach 15s; writeIndex chỉ emit audio + stall log. | Probe G3.1 PASS (hb 10Hz; kill bridge → restart 3.8s, log stalled); G2.5 stress PASS; G1.5 stress PASS (fix test script đọc log 1 lần/poll); G0.4 probe PASS. | G1.5 fail trước đó là test-env: test script gọi log_lines() mỗi index (140k dòng) + window VST cũ dính → OPEN_GUI dedupe silent; flood 896 dòng allNotesOff pre-existing. | Không cần fix thêm. | ✅ | +| G3.2 | | | | | ⬜ | +| G3.3 | | | | | ⬜ | + +### Nhật ký thực hiện + +### G3.1 — Hoàn tất (2026-08-15) +- SHM: `heartbeat` append @11164, tổng 11168B (SharedMemoryIPC.h + shm.rs field/accessor/assert + shm_selfcheck.cpp assert). +- main.cpp: thread riêng `Sleep(100)` → `shmIPC->heartbeat++` (độc lập audio loop). +- lib.rs pump: liveness = heartbeat, deadline 3s → kill+spawn 1 lần/bridge-down; bỏ attach_deadline 15s; writeIndex chỉ emit audio; heartbeat còn + writeIndex đứng → log `[tauri] audio stall (heartbeat OK)`, không restart. +- Verify: probe heartbeat PASS (tăng ~10Hz, kill → watchdog restart 3.8s, spawn.log `bridge heartbeat stalled 3s — restart attempt #1`); G2.5 stress PASS; G1.5 stress PASS; G0.4 probe PASS. +- G1.5 fail trước đó = test-env, không phải regression G3.1: (1) test script `wait_log` đọc cả file log mỗi index truy cập (140k+ dòng) → poll chậm >10s → STOP/OPEN_GUI timeout giả; fix: đọc 1 lần/poll. (2) window VST cũ (0x870D76, 0x2F0754) giữ view giữa các lần chạy → OPEN_GUI dedupe silent (handleOpenGui `hasAttachedView()` return không log). (3) flood 896 dòng `[midi] OFF` = allNotesOff() log từng pitch (7 ch × 128) — pre-existing, STOP log vẫn 0.15s. +- G3.2 (phân loại stall), G3.3 (SEH từng channel): chưa làm. diff --git a/native_bridge/include/SharedMemoryIPC.h b/native_bridge/include/SharedMemoryIPC.h index c32224f..405ebb9 100644 --- a/native_bridge/include/SharedMemoryIPC.h +++ b/native_bridge/include/SharedMemoryIPC.h @@ -55,4 +55,10 @@ struct SharedAudioBufferIPC { // to extend its stall deadline — a long Nexus attach must not look like // a dead bridge. Append-only field: existing offsets stay stable. volatile uint32_t attachInFlight; + + // G3.1: dedicated heartbeat, incremented every 100ms by a bridge-side + // thread INDEPENDENT of the audio loop. The Tauri watchdog keys bridge + // liveness on this, so a slow (but alive) audio loop no longer looks dead. + // Append-only field: existing offsets stay stable. + volatile uint32_t heartbeat; }; diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 2cf8781..c4f68fd 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -657,6 +657,17 @@ int main(int argc, char* argv[]) { if (!shmIPC) return 1; #endif + // G3.1: dedicated heartbeat thread, independent of the audio loop. + // Increments every 100ms (~10Hz). Tauri watchdog keys liveness on this. + // Started right after the SHM map so a slow instrument restore at + // startup (or after watchdog respawn) never looks like a dead bridge. + std::thread([shmIPC]() { + for (;;) { + Sleep(100); + shmIPC->heartbeat++; + } + }).detach(); + InstrumentEngineManager instruments; #ifdef _WIN32 g_engine = &instruments; diff --git a/native_bridge/tests/shm_selfcheck.cpp b/native_bridge/tests/shm_selfcheck.cpp index 17fc6e7..1a2aade 100644 --- a/native_bridge/tests/shm_selfcheck.cpp +++ b/native_bridge/tests/shm_selfcheck.cpp @@ -65,6 +65,7 @@ int main() { // 5. Layout stability — MUST match Rust src-tauri/src/shm.rs assert(sizeof(SharedAudioBufferIPC::MidiEventIPC) == 12); assert(sizeof(SharedAudioBufferIPC::ControlEventIPC) == 1040); + assert(sizeof(SharedAudioBufferIPC) == 11168); // G3.1: +4 heartbeat std::printf("SHM self-check OK (sizeof struct = %zu bytes)\n", sizeof(SharedAudioBufferIPC)); shm_close(h); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8fcb48c..4b28a42 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -374,13 +374,19 @@ pub fn run() { let _ = spawn_bridge(&app_handle, &res_dir, &exe_dir, &mut log_line, &spawn_log_path); // ── Audio pump: bridge SHM -> WebView `bridge-audio` events ── - // B10 health: if bridgeWriteIndex stalls for 3s the bridge is dead — - // respawn once, then emit `bridge-down` so the UI falls back to WASM. + // G3.1: bridge liveness keys on the DEDICATED heartbeat (a bridge + // thread, independent of the audio loop). A slow-but-alive audio + // loop no longer looks dead. writeIndex is now only for audio + // emission; a stalled writeIndex with a live heartbeat is logged + // as an audio stall, never a restart. let pump_handle = app.handle().clone(); std::thread::spawn(move || { let mut last_index: u32 = 0; - let mut last_change = std::time::Instant::now(); + let mut last_hb: u32 = 0; + let mut last_hb_change = std::time::Instant::now(); + let mut last_audio_change = std::time::Instant::now(); let mut down_emitted = false; + let mut stall_logged = false; let mut restart_attempts = 0u32; loop { // App đang tắt (window Destroyed) — dừng pump, không respawn bridge. @@ -395,7 +401,25 @@ pub fn run() { continue; } }; - let advanced = guard.as_ref().map(|shm| { + // Liveness = heartbeat advancing (~10Hz from bridge thread). + let hb_advanced = guard.as_ref().map(|shm| { + let hb = shm.heartbeat(); + if hb != last_hb { + last_hb = hb; + true + } else { + false + } + }); + if hb_advanced == Some(true) { + last_hb_change = std::time::Instant::now(); + down_emitted = false; + // Bridge sống lại sau restart — đóng "sự cố" để lần + // stall sau lại được hưởng 1 restart (mỗi incident 1 lần). + restart_attempts = 0; + } + // Audio emission = writeIndex advancing (audio loop alive). + let audio_advanced = guard.as_ref().map(|shm| { let idx = shm.write_index(); if idx != last_index { last_index = idx; @@ -404,33 +428,41 @@ pub fn run() { false } }); - // G0.3: an openGUI attach job (reload/attachView on the - // bridge worker) can legitimately outlast the 3s stall - // deadline if it blocks the audio loop's message pump — - // do not kill the bridge for that. Real stalls are still - // caught: attach never takes > 15s; a dead bridge leaves - // the flag stuck and is restarted after the longer wait. - let attach_deadline = guard - .as_ref() - .map(|s| s.attach_in_flight()) - .unwrap_or(false); - let stall_deadline = if attach_deadline { - std::time::Duration::from_secs(15) - } else { - std::time::Duration::from_secs(3) - }; - if advanced == Some(true) { - last_change = std::time::Instant::now(); - down_emitted = false; - // Bridge đã sống lại sau restart — đóng "sự cố" để lần - // stall sau lại được hưởng 1 restart (mỗi incident 1 lần). - restart_attempts = 0; + if audio_advanced == Some(true) { + last_audio_change = std::time::Instant::now(); + stall_logged = false; let (l, r) = guard.as_ref().map(|s| s.read_audio()).unwrap_or(([0f32; shm::AUDIO_BLOCK_SIZE], [0f32; shm::AUDIO_BLOCK_SIZE])); let _ = pump_handle.emit( "bridge-audio", AudioFrame { l: l.to_vec(), r: r.to_vec() }, ); - } else if last_change.elapsed() >= stall_deadline && !down_emitted { + } + // G3.1 FIX: nhả guard TRƯỚC stall log / watchdog kill+spawn. + // kill_bridge -> flush_bridge_state khóa chính ShmState mutex + // mà pump đang giữ -> deadlock khi watchdog trigger (không + // restart, không log, bridge chết vĩnh viễn). + drop(guard); + if audio_advanced != Some(true) + && hb_advanced == Some(true) + && last_audio_change.elapsed() >= std::time::Duration::from_secs(3) + && !stall_logged + { + // Heartbeat OK nhưng audio loop không push block — stall + // thực sự (không phải bridge chết). Log, không restart. + stall_logged = true; + let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); + let bridge_log = std::path::Path::new(&log_dir) + .join("SonicForgeDAW").join("logs").join("bridge.log"); + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true).append(true).open(&bridge_log) + { + use std::io::Write; + let _ = f.write_all(b"[tauri] audio stall (heartbeat OK)\n"); + } + } + if hb_advanced == Some(false) && !down_emitted + && last_hb_change.elapsed() >= std::time::Duration::from_secs(3) + { // Check lai truoc khi respawn — Destroyed co the vua set flag. if SHUTTING_DOWN.load(Ordering::Relaxed) { break; @@ -446,7 +478,8 @@ pub fn run() { let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let log_path = std::path::Path::new(&log_dir) .join("SonicForgeDAW").join("logs").join("spawn.log"); - let mut line = String::from("[tauri] bridge stalled 3s — restart attempt #1\n"); + let mut line = String::from("[tauri] bridge heartbeat stalled 3s — restart attempt #1 +"); // FIX: kill bridge cũ trước — không được để 2 bridge // cùng map SHM (race control queue / double load). kill_bridge(&pump_handle); @@ -458,9 +491,10 @@ pub fn run() { .create(true).append(true).open(&bridge_log) { use std::io::Write; - let _ = f.write_all(b"[tauri] bridge restarted (attempt 1)\n"); + let _ = f.write_all(b"[tauri] bridge restarted (attempt 1) +"); } - last_change = std::time::Instant::now(); + last_hb_change = std::time::Instant::now(); down_emitted = false; } else { let _ = pump_handle.emit("bridge-down", ()); @@ -469,7 +503,6 @@ pub fn run() { let _ = pump_handle.emit("bridge-down", ()); } } - drop(guard); std::thread::sleep(std::time::Duration::from_millis(1)); } }); diff --git a/src-tauri/src/shm.rs b/src-tauri/src/shm.rs index 01cecdb..2fbb4e4 100644 --- a/src-tauri/src/shm.rs +++ b/src-tauri/src/shm.rs @@ -63,6 +63,10 @@ pub struct SharedAudioBufferIPC { // Append-only field — offsets of control_queue (2836) and // control_queue_count (11156) are unchanged. pub attach_in_flight: u32, + // G3.1: dedicated heartbeat — incremented ~10Hz by a bridge thread + // independent of the audio loop. Watchdog liveness keys on this. + // Append-only field: offsets above are unchanged. + pub heartbeat: u32, } pub struct Shm { @@ -166,6 +170,10 @@ impl Shm { pub fn attach_in_flight(&self) -> bool { self.ipc().attach_in_flight != 0 } + + pub fn heartbeat(&self) -> u32 { + self.ipc().heartbeat + } } impl Drop for Shm { @@ -188,6 +196,6 @@ mod tests { // native_bridge/tests/shm_selfcheck.cpp asserts the same sizes. assert_eq!(std::mem::size_of::(), 12); assert_eq!(std::mem::size_of::(), 1040); - assert_eq!(std::mem::size_of::(), 11164); + assert_eq!(std::mem::size_of::(), 11168); } }