From 05404c6e7a82df5a52c28e7accb6a6f3f40e860a Mon Sep 17 00:00:00 2001 From: Chummy Date: Thu, 19 Feb 2026 21:01:26 +0800 Subject: [PATCH] perf(build): gate Matrix channel for faster iteration --- Cargo.lock | 44 ++++++----------------------------- Cargo.toml | 9 ++++---- docs/channels-reference.md | 19 +++++++++++++++ docs/troubleshooting.md | 47 +++++++++++++++++++++++++++++++++++++- src/channels/dingtalk.rs | 6 ++--- src/channels/discord.rs | 10 ++++---- src/channels/lark.rs | 14 +++++++----- src/channels/mod.rs | 28 ++++++++++++++++++++++- src/channels/qq.rs | 18 +++++++++++---- 9 files changed, 135 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b43472..1305b65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,7 +408,7 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.28.0", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -6004,9 +6004,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.24.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -6014,22 +6014,10 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tungstenite 0.24.0", + "tungstenite", "webpki-roots 0.26.11", ] -[[package]] -name = "tokio-tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.28.0", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -6333,26 +6321,6 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -[[package]] -name = "tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.4.0", - "httparse", - "log", - "rand 0.8.5", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 1.0.69", - "utf-8", -] - [[package]] name = "tungstenite" version = "0.28.0" @@ -6365,6 +6333,8 @@ dependencies = [ "httparse", "log", "rand 0.9.2", + "rustls", + "rustls-pki-types", "sha1", "thiserror 2.0.18", "utf-8", @@ -7747,7 +7717,7 @@ dependencies = [ "tokio-rustls", "tokio-serial", "tokio-stream", - "tokio-tungstenite 0.24.0", + "tokio-tungstenite", "tokio-util", "toml 1.0.1+spec-1.1.0", "tower", diff --git a/Cargo.toml b/Cargo.toml index 4f42ec0..7c4aba1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tokio-util = { version = "0.7", default-features = false } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking", "multipart", "stream", "socks"] } # Matrix client + E2EE decryption -matrix-sdk = { version = "0.16", default-features = false, features = ["e2e-encryption", "rustls-tls", "markdown"] } +matrix-sdk = { version = "0.16", optional = true, default-features = false, features = ["e2e-encryption", "rustls-tls", "markdown"] } # Serialization serde = { version = "1.0", default-features = false, features = ["derive"] } @@ -103,8 +103,8 @@ console = "0.16" # Hardware discovery (device path globbing) glob = "0.3" -# Discord WebSocket gateway -tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] } +# WebSocket client channels (Discord/Lark/DingTalk) +tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } futures = "0.3" regex = "1.10" @@ -158,8 +158,9 @@ rppal = { version = "0.22", optional = true } landlock = { version = "0.4", optional = true } [features] -default = ["hardware"] +default = ["hardware", "channel-matrix"] hardware = ["nusb", "tokio-serial"] +channel-matrix = ["dep:matrix-sdk"] peripheral-rpi = ["rppal"] # Browser backend feature alias used by cfg(feature = "browser-native") browser-native = ["dep:fantoccini"] diff --git a/docs/channels-reference.md b/docs/channels-reference.md index a25050c..61d0604 100644 --- a/docs/channels-reference.md +++ b/docs/channels-reference.md @@ -69,6 +69,25 @@ Operational notes: ## Channel Matrix +### Build Feature Toggle (`channel-matrix`) + +Matrix support is controlled at compile time by the `channel-matrix` Cargo feature. + +- Default builds include Matrix support (`default = ["hardware", "channel-matrix"]`). +- For faster local iteration when Matrix is not needed: + +```bash +cargo check --no-default-features --features hardware +``` + +- To explicitly enable Matrix support in custom feature sets: + +```bash +cargo check --no-default-features --features hardware,channel-matrix +``` + +If `[channels_config.matrix]` is present but the binary was built without `channel-matrix`, `zeroclaw channel list`, `zeroclaw channel doctor`, and `zeroclaw channel start` will log that Matrix is intentionally skipped for this build. + --- ## 2. Delivery Modes at a Glance diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e06e74a..ab7cfbf 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2,7 +2,7 @@ This guide focuses on common setup/runtime failures and fast resolution paths. -Last verified: **February 18, 2026**. +Last verified: **February 19, 2026**. ## Installation / Bootstrap @@ -32,6 +32,51 @@ Fix: ./bootstrap.sh --install-system-deps ``` +### Build is very slow or appears stuck + +Symptoms: + +- `cargo check` / `cargo build` appears stuck at `Checking zeroclaw` for a long time +- repeated `Blocking waiting for file lock on package cache` or `build directory` + +Why this happens in ZeroClaw: + +- Matrix E2EE stack (`matrix-sdk`, `ruma`, `vodozemac`) is large and expensive to type-check. +- TLS + crypto native build scripts (`aws-lc-sys`, `ring`) add noticeable compile time. +- `rusqlite` with bundled SQLite compiles C code locally. +- Running multiple cargo jobs/worktrees in parallel causes lock contention. + +Fast checks: + +```bash +cargo check --timings +cargo tree -d +``` + +The timing report is written to `target/cargo-timings/cargo-timing.html`. + +Faster local iteration (when Matrix channel is not needed): + +```bash +cargo check --no-default-features --features hardware +``` + +This skips `channel-matrix` and can significantly reduce compile time. + +To build with Matrix support explicitly enabled: + +```bash +cargo check --no-default-features --features hardware,channel-matrix +``` + +Lock-contention mitigation: + +```bash +pgrep -af "cargo (check|build|test)|cargo check|cargo build|cargo test" +``` + +Stop unrelated cargo jobs before running your own build. + ### `zeroclaw` command not found after install Symptom: diff --git a/src/channels/dingtalk.rs b/src/channels/dingtalk.rs index 54a8097..44fd49c 100644 --- a/src/channels/dingtalk.rs +++ b/src/channels/dingtalk.rs @@ -169,7 +169,7 @@ impl Channel for DingTalkChannel { _ => continue, }; - let frame: serde_json::Value = match serde_json::from_str(&msg) { + let frame: serde_json::Value = match serde_json::from_str(msg.as_ref()) { Ok(v) => v, Err(_) => continue, }; @@ -195,7 +195,7 @@ impl Channel for DingTalkChannel { "data": "", }); - if let Err(e) = write.send(Message::Text(pong.to_string())).await { + if let Err(e) = write.send(Message::Text(pong.to_string().into())).await { tracing::warn!("DingTalk: failed to send pong: {e}"); break; } @@ -262,7 +262,7 @@ impl Channel for DingTalkChannel { "message": "OK", "data": "", }); - let _ = write.send(Message::Text(ack.to_string())).await; + let _ = write.send(Message::Text(ack.to_string().into())).await; let channel_msg = ChannelMessage { id: Uuid::new_v4().to_string(), diff --git a/src/channels/discord.rs b/src/channels/discord.rs index de31cd0..3ff8243 100644 --- a/src/channels/discord.rs +++ b/src/channels/discord.rs @@ -272,7 +272,9 @@ impl Channel for DiscordChannel { } } }); - write.send(Message::Text(identify.to_string())).await?; + write + .send(Message::Text(identify.to_string().into())) + .await?; tracing::info!("Discord: connected and identified"); @@ -301,7 +303,7 @@ impl Channel for DiscordChannel { _ = hb_rx.recv() => { let d = if sequence >= 0 { json!(sequence) } else { json!(null) }; let hb = json!({"op": 1, "d": d}); - if write.send(Message::Text(hb.to_string())).await.is_err() { + if write.send(Message::Text(hb.to_string().into())).await.is_err() { break; } } @@ -312,7 +314,7 @@ impl Channel for DiscordChannel { _ => continue, }; - let event: serde_json::Value = match serde_json::from_str(&msg) { + let event: serde_json::Value = match serde_json::from_str(msg.as_ref()) { Ok(e) => e, Err(_) => continue, }; @@ -329,7 +331,7 @@ impl Channel for DiscordChannel { 1 => { let d = if sequence >= 0 { json!(sequence) } else { json!(null) }; let hb = json!({"op": 1, "d": d}); - if write.send(Message::Text(hb.to_string())).await.is_err() { + if write.send(Message::Text(hb.to_string().into())).await.is_err() { break; } continue; diff --git a/src/channels/lark.rs b/src/channels/lark.rs index 40b2569..c899097 100644 --- a/src/channels/lark.rs +++ b/src/channels/lark.rs @@ -288,7 +288,7 @@ impl LarkChannel { payload: None, }; if write - .send(WsMsg::Binary(initial_ping.encode_to_vec())) + .send(WsMsg::Binary(initial_ping.encode_to_vec().into())) .await .is_err() { @@ -309,7 +309,7 @@ impl LarkChannel { headers: vec![PbHeader { key: "type".into(), value: "ping".into() }], payload: None, }; - if write.send(WsMsg::Binary(ping.encode_to_vec())).await.is_err() { + if write.send(WsMsg::Binary(ping.encode_to_vec().into())).await.is_err() { tracing::warn!("Lark: ping failed, reconnecting"); break; } @@ -378,7 +378,7 @@ impl LarkChannel { let mut ack = frame.clone(); ack.payload = Some(br#"{"code":200,"headers":{},"data":[]}"#.to_vec()); ack.headers.push(PbHeader { key: "biz_rt".into(), value: "0".into() }); - let _ = write.send(WsMsg::Binary(ack.encode_to_vec())).await; + let _ = write.send(WsMsg::Binary(ack.encode_to_vec().into())).await; } // Fragment reassembly @@ -917,9 +917,11 @@ mod tests { #[test] fn lark_ws_activity_refreshes_heartbeat_watchdog() { - assert!(should_refresh_last_recv(&WsMsg::Binary(vec![1, 2, 3]))); - assert!(should_refresh_last_recv(&WsMsg::Ping(vec![9, 9]))); - assert!(should_refresh_last_recv(&WsMsg::Pong(vec![8, 8]))); + assert!(should_refresh_last_recv(&WsMsg::Binary( + vec![1, 2, 3].into() + ))); + assert!(should_refresh_last_recv(&WsMsg::Ping(vec![9, 9].into()))); + assert!(should_refresh_last_recv(&WsMsg::Pong(vec![8, 8].into()))); } #[test] diff --git a/src/channels/mod.rs b/src/channels/mod.rs index ba55956..40361b3 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -6,6 +6,7 @@ pub mod imessage; pub mod irc; pub mod lark; pub mod linq; +#[cfg(feature = "channel-matrix")] pub mod matrix; pub mod mattermost; pub mod qq; @@ -27,6 +28,7 @@ pub use imessage::IMessageChannel; pub use irc::IrcChannel; pub use lark::LarkChannel; pub use linq::LinqChannel; +#[cfg(feature = "channel-matrix")] pub use matrix::MatrixChannel; pub use mattermost::MattermostChannel; pub use qq::QQChannel; @@ -1389,7 +1391,10 @@ pub async fn handle_command(command: crate::ChannelCommands, config: &Config) -> ("Mattermost", config.channels_config.mattermost.is_some()), ("Webhook", config.channels_config.webhook.is_some()), ("iMessage", config.channels_config.imessage.is_some()), - ("Matrix", config.channels_config.matrix.is_some()), + ( + "Matrix", + cfg!(feature = "channel-matrix") && config.channels_config.matrix.is_some(), + ), ("Signal", config.channels_config.signal.is_some()), ("WhatsApp", config.channels_config.whatsapp.is_some()), ("Linq", config.channels_config.linq.is_some()), @@ -1401,6 +1406,11 @@ pub async fn handle_command(command: crate::ChannelCommands, config: &Config) -> ] { println!(" {} {name}", if configured { "✅" } else { "❌" }); } + if !cfg!(feature = "channel-matrix") { + println!( + " â„šī¸ Matrix channel support is disabled in this build (enable `channel-matrix`)." + ); + } println!("\nTo start channels: zeroclaw channel start"); println!("To check health: zeroclaw channel doctor"); println!("To configure: zeroclaw onboard"); @@ -1489,6 +1499,7 @@ pub async fn doctor_channels(config: Config) -> Result<()> { )); } + #[cfg(feature = "channel-matrix")] if let Some(ref mx) = config.channels_config.matrix { channels.push(( "Matrix", @@ -1503,6 +1514,13 @@ pub async fn doctor_channels(config: Config) -> Result<()> { )); } + #[cfg(not(feature = "channel-matrix"))] + if config.channels_config.matrix.is_some() { + tracing::warn!( + "Matrix channel is configured but this build was compiled without `channel-matrix`; skipping Matrix health check." + ); + } + if let Some(ref sig) = config.channels_config.signal { channels.push(( "Signal", @@ -1864,6 +1882,7 @@ pub async fn start_channels(config: Config) -> Result<()> { channels.push(Arc::new(IMessageChannel::new(im.allowed_contacts.clone()))); } + #[cfg(feature = "channel-matrix")] if let Some(ref mx) = config.channels_config.matrix { channels.push(Arc::new(MatrixChannel::new_with_session_hint( mx.homeserver.clone(), @@ -1875,6 +1894,13 @@ pub async fn start_channels(config: Config) -> Result<()> { ))); } + #[cfg(not(feature = "channel-matrix"))] + if config.channels_config.matrix.is_some() { + tracing::warn!( + "Matrix channel is configured but this build was compiled without `channel-matrix`; skipping Matrix runtime startup." + ); + } + if let Some(ref sig) = config.channels_config.signal { channels.push(Arc::new(SignalChannel::new( sig.http_url.clone(), diff --git a/src/channels/qq.rs b/src/channels/qq.rs index e0231f8..18117ef 100644 --- a/src/channels/qq.rs +++ b/src/channels/qq.rs @@ -263,7 +263,9 @@ impl Channel for QQChannel { } } }); - write.send(Message::Text(identify.to_string())).await?; + write + .send(Message::Text(identify.to_string().into())) + .await?; tracing::info!("QQ: connected and identified"); @@ -287,7 +289,11 @@ impl Channel for QQChannel { _ = hb_rx.recv() => { let d = if sequence >= 0 { json!(sequence) } else { json!(null) }; let hb = json!({"op": 1, "d": d}); - if write.send(Message::Text(hb.to_string())).await.is_err() { + if write + .send(Message::Text(hb.to_string().into())) + .await + .is_err() + { break; } } @@ -298,7 +304,7 @@ impl Channel for QQChannel { _ => continue, }; - let event: serde_json::Value = match serde_json::from_str(&msg) { + let event: serde_json::Value = match serde_json::from_str(msg.as_ref()) { Ok(e) => e, Err(_) => continue, }; @@ -315,7 +321,11 @@ impl Channel for QQChannel { 1 => { let d = if sequence >= 0 { json!(sequence) } else { json!(null) }; let hb = json!({"op": 1, "d": d}); - if write.send(Message::Text(hb.to_string())).await.is_err() { + if write + .send(Message::Text(hb.to_string().into())) + .await + .is_err() + { break; } continue;