Skip to content

Commit d29dbf7

Browse files
authored
perf(tauri): Rust-native desktop event transport (#242)
## Summary - switch the Tauri desktop runtime from the browser `EventSource` path to a native Rust desktop event transport while leaving browser and Electron unchanged - restore SSE heartbeat parity by parsing named `event:` frames and replying to `codenomad.client.ping` with an authenticated `/api/client-connections/pong` - add a Tauri-only settings toggle that lets the current device fall back to the browser `EventSource` transport without leaking that choice through shared config - remove the temporary benchmark harness from the shipped code now that the transport behavior has been validated ## Benchmark The temporary in-app benchmark harness used during validation has been removed from the final code, but the measured results are retained here for review context. Real Tauri/WebView2 benchmark on Windows using the dedicated session: - workspace: `D:\CodeNomad` - session: `ses_21feb15b3ffeLz3uRModK4KKnG` Short command: - `node -e "for (let i = 1; i <= 400; i += 1) console.log('line ' + i)"` Results: - browser `EventSource` forced in Tauri: - timed out after `131479.7ms` - `sawWorking=false` - `reachedIdle=false` - `batchesReceived=84` - `eventsReceived=84` - `maxBatchSize=1` - Rust-native transport: - completed in `1437.4ms` - `sawWorking=true` - `reachedIdle=true` - `batchesReceived=4` - `eventsReceived=45` - `maxBatchSize=27` Long heartbeat / stale-timeout validation: - command: `powershell -NoProfile -Command Start-Sleep -Seconds 70` - Rust-native transport: - completed in `71689.5ms` - `sawWorking=true` - `reachedIdle=true` - `batchesReceived=13` - `eventsReceived=72` - `maxBatchSize=25` Confirmed separately afterward: the native transport also behaves better on Linux. ## Validation - `cargo test named_ping_event_is_routed_to_ping_channel` - `cargo test session_cookie_is_attached_to_requests` - `cargo test --no-run` - `npx tsc --noEmit --pretty -p packages/ui/tsconfig.json` - `npx tsc --noEmit --pretty -p packages/server/tsconfig.json` - manual Tauri/WebView2 benchmark on Windows - manual confirmation on Linux after the benchmark phase ## Notes - this remains a Tauri-only transport; browser and Electron stay on the browser `EventSource` path - the Tauri fallback toggle is now genuinely device-local and restarts the local event stream immediately when changed - the long run validates heartbeat / stale-timeout robustness, not headline perf
1 parent b2f3e14 commit d29dbf7

25 files changed

Lines changed: 2419 additions & 65 deletions

packages/tauri-app/src-tauri/src/cli_manager.rs

Lines changed: 154 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::desktop_event_transport::DesktopEventStreamConfig;
12
use crate::managed_node::resolve_bundled_node_binary;
23
use dirs::home_dir;
34
use parking_lot::Mutex;
@@ -185,12 +186,13 @@ fn kill_process_tree_windows(pid: u32, force: bool) -> bool {
185186
}
186187
fn navigate_main(app: &AppHandle, url: &str) {
187188
if let Some(win) = app.webview_windows().get("main") {
188-
let mut display = url.to_string();
189+
let final_url = augment_launch_url(url);
190+
let mut display = final_url.clone();
189191
if let Some(hash_index) = display.find('#') {
190192
display.replace_range(hash_index + 1.., "[REDACTED]");
191193
}
192194
log_line(&format!("navigating main to {display}"));
193-
if let Ok(parsed) = Url::parse(url) {
195+
if let Ok(parsed) = Url::parse(&final_url) {
194196
let _ = win.navigate(parsed);
195197
} else {
196198
log_line("failed to parse URL for navigation");
@@ -200,6 +202,31 @@ fn navigate_main(app: &AppHandle, url: &str) {
200202
}
201203
}
202204

205+
fn augment_launch_url(base_url: &str) -> String {
206+
let launch_query = std::env::var("CODENOMAD_UI_LAUNCH_QUERY")
207+
.ok()
208+
.map(|value| value.trim().to_string())
209+
.filter(|value| !value.is_empty());
210+
211+
let Some(launch_query) = launch_query else {
212+
return base_url.to_string();
213+
};
214+
215+
if base_url.contains('?') {
216+
return format!(
217+
"{}&{}",
218+
base_url,
219+
launch_query.trim_start_matches(['?', '#'])
220+
);
221+
}
222+
223+
format!(
224+
"{}?{}",
225+
base_url,
226+
launch_query.trim_start_matches(['?', '#'])
227+
)
228+
}
229+
203230
fn extract_cookie_value(set_cookie: &str, name: &str) -> Option<String> {
204231
let prefix = format!("{name}=");
205232
let cookie_kv = set_cookie.split(';').next()?.trim();
@@ -298,6 +325,15 @@ fn generate_auth_cookie_name() -> String {
298325
format!("{SESSION_COOKIE_NAME_PREFIX}_{pid}_{timestamp}")
299326
}
300327

328+
fn generate_transport_connection_id() -> String {
329+
let ts = SystemTime::now()
330+
.duration_since(UNIX_EPOCH)
331+
.unwrap_or_default()
332+
.as_millis();
333+
let tid = std::thread::current().id();
334+
format!("tauri-{}-{:?}", ts, tid)
335+
}
336+
301337
const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";
302338

303339
#[derive(Debug, Deserialize)]
@@ -456,6 +492,8 @@ pub struct CliProcessManager {
456492
job: Arc<Mutex<Option<WindowsJobObject>>>,
457493
ready: Arc<AtomicBool>,
458494
bootstrap_token: Arc<Mutex<Option<String>>>,
495+
session_cookie: Arc<Mutex<Option<String>>>,
496+
auth_cookie_name: Arc<Mutex<Option<String>>>,
459497
}
460498

461499
impl CliProcessManager {
@@ -467,6 +505,8 @@ impl CliProcessManager {
467505
job: Arc::new(Mutex::new(None)),
468506
ready: Arc::new(AtomicBool::new(false)),
469507
bootstrap_token: Arc::new(Mutex::new(None)),
508+
session_cookie: Arc::new(Mutex::new(None)),
509+
auth_cookie_name: Arc::new(Mutex::new(None)),
470510
}
471511
}
472512

@@ -475,6 +515,8 @@ impl CliProcessManager {
475515
self.stop()?;
476516
self.ready.store(false, Ordering::SeqCst);
477517
*self.bootstrap_token.lock() = None;
518+
*self.session_cookie.lock() = None;
519+
*self.auth_cookie_name.lock() = None;
478520
{
479521
let mut status = self.status.lock();
480522
status.state = CliState::Starting;
@@ -491,6 +533,8 @@ impl CliProcessManager {
491533
let job_arc = self.job.clone();
492534
let ready_flag = self.ready.clone();
493535
let token_arc = self.bootstrap_token.clone();
536+
let session_cookie_arc = self.session_cookie.clone();
537+
let auth_cookie_name_arc = self.auth_cookie_name.clone();
494538
thread::spawn(move || {
495539
if let Err(err) = Self::spawn_cli(
496540
app.clone(),
@@ -500,6 +544,8 @@ impl CliProcessManager {
500544
job_arc,
501545
ready_flag,
502546
token_arc,
547+
session_cookie_arc,
548+
auth_cookie_name_arc,
503549
dev,
504550
) {
505551
log_line(&format!("cli spawn failed: {err}"));
@@ -594,6 +640,7 @@ impl CliProcessManager {
594640
status.port = None;
595641
status.url = None;
596642
status.error = None;
643+
*self.session_cookie.lock() = None;
597644

598645
Ok(())
599646
}
@@ -602,13 +649,35 @@ impl CliProcessManager {
602649
self.status.lock().clone()
603650
}
604651

652+
pub fn desktop_event_stream_config(&self) -> Option<DesktopEventStreamConfig> {
653+
let base_url = self.status.lock().url.clone()?;
654+
let events_url = format!("{}/api/events", base_url.trim_end_matches('/'));
655+
let client_id = format!("tauri-{}", std::process::id());
656+
let cookie_name = self
657+
.auth_cookie_name
658+
.lock()
659+
.clone()
660+
.unwrap_or_else(|| SESSION_COOKIE_NAME_PREFIX.to_string());
661+
662+
Some(DesktopEventStreamConfig {
663+
base_url,
664+
events_url,
665+
client_id,
666+
connection_id: generate_transport_connection_id(),
667+
cookie_name,
668+
session_cookie: self.session_cookie.lock().clone(),
669+
})
670+
}
671+
605672
fn spawn_cli(
606673
app: AppHandle,
607674
status: Arc<Mutex<CliStatus>>,
608675
child_holder: Arc<Mutex<Option<Child>>>,
609676
#[cfg(windows)] job_holder: Arc<Mutex<Option<WindowsJobObject>>>,
610677
ready: Arc<AtomicBool>,
611678
bootstrap_token: Arc<Mutex<Option<String>>>,
679+
session_cookie: Arc<Mutex<Option<String>>>,
680+
auth_cookie_name_holder: Arc<Mutex<Option<String>>>,
612681
dev: bool,
613682
) -> anyhow::Result<()> {
614683
log_line("resolving CLI entry");
@@ -619,6 +688,7 @@ impl CliProcessManager {
619688
resolution.runner, resolution.entry, host
620689
));
621690
let auth_cookie_name = Arc::new(generate_auth_cookie_name());
691+
*auth_cookie_name_holder.lock() = Some(auth_cookie_name.as_str().to_string());
622692
let args = resolution.build_args(dev, &host, auth_cookie_name.as_str());
623693
log_line(&format!("CLI args: {:?}", args));
624694
if dev {
@@ -723,6 +793,7 @@ impl CliProcessManager {
723793
let app_clone = app.clone();
724794
let ready_clone = ready.clone();
725795
let token_clone = bootstrap_token.clone();
796+
let session_cookie_clone = session_cookie.clone();
726797
let auth_cookie_name_clone = auth_cookie_name.clone();
727798

728799
thread::spawn(move || {
@@ -742,6 +813,7 @@ impl CliProcessManager {
742813
let status = status_clone.clone();
743814
let ready = ready_clone.clone();
744815
let token = token_clone.clone();
816+
let session_cookie = session_cookie_clone.clone();
745817
let auth_cookie_name = auth_cookie_name_clone.clone();
746818
thread::spawn(move || {
747819
Self::process_stream(
@@ -751,6 +823,7 @@ impl CliProcessManager {
751823
&status,
752824
&ready,
753825
&token,
826+
&session_cookie,
754827
auth_cookie_name.as_str(),
755828
);
756829
});
@@ -761,6 +834,7 @@ impl CliProcessManager {
761834
let status = status_clone.clone();
762835
let ready = ready_clone.clone();
763836
let token = token_clone.clone();
837+
let session_cookie = session_cookie_clone.clone();
764838
let auth_cookie_name = auth_cookie_name_clone.clone();
765839
thread::spawn(move || {
766840
Self::process_stream(
@@ -770,6 +844,7 @@ impl CliProcessManager {
770844
&status,
771845
&ready,
772846
&token,
847+
&session_cookie,
773848
auth_cookie_name.as_str(),
774849
);
775850
});
@@ -894,6 +969,7 @@ impl CliProcessManager {
894969
status: &Arc<Mutex<CliStatus>>,
895970
ready: &Arc<AtomicBool>,
896971
bootstrap_token: &Arc<Mutex<Option<String>>>,
972+
session_cookie: &Arc<Mutex<Option<String>>>,
897973
auth_cookie_name: &str,
898974
) {
899975
let mut buffer = String::new();
@@ -946,6 +1022,7 @@ impl CliProcessManager {
9461022
status,
9471023
ready,
9481024
bootstrap_token,
1025+
session_cookie,
9491026
auth_cookie_name,
9501027
url,
9511028
);
@@ -963,6 +1040,7 @@ impl CliProcessManager {
9631040
status: &Arc<Mutex<CliStatus>>,
9641041
ready: &Arc<AtomicBool>,
9651042
bootstrap_token: &Arc<Mutex<Option<String>>>,
1043+
session_cookie: &Arc<Mutex<Option<String>>>,
9661044
auth_cookie_name: &str,
9671045
base_url: String,
9681046
) {
@@ -995,6 +1073,7 @@ impl CliProcessManager {
9951073
log_line(&format!("failed to set session cookie: {err}"));
9961074
navigate_main(app, &format!("{base_url}/login"));
9971075
} else {
1076+
*session_cookie.lock() = Some(session_id.clone());
9981077
navigate_main(app, &base_url);
9991078
}
10001079
}
@@ -1215,31 +1294,37 @@ fn resolve_dev_entry(_app: &AppHandle) -> Option<String> {
12151294
}
12161295

12171296
fn resolve_prod_entry(_app: &AppHandle) -> Option<String> {
1297+
let base = workspace_root();
1298+
let exe_dir = std::env::current_exe()
1299+
.ok()
1300+
.and_then(|exe| exe.parent().map(|dir| dir.to_path_buf()));
1301+
1302+
first_existing(prod_entry_candidates(exe_dir, base))
1303+
}
1304+
1305+
fn prod_entry_candidates(
1306+
exe_dir: Option<PathBuf>,
1307+
workspace: Option<PathBuf>,
1308+
) -> Vec<Option<PathBuf>> {
12181309
let mut candidates = Vec::new();
12191310

1220-
if let Ok(exe) = std::env::current_exe() {
1221-
if let Some(dir) = exe.parent() {
1222-
candidates.push(Some(dir.join("resources/server/dist/bin.js")));
1311+
if let Some(dir) = exe_dir {
1312+
candidates.push(Some(dir.join("resources/server/dist/bin.js")));
12231313

1224-
let resources = dir.join("../Resources");
1225-
candidates.push(Some(resources.join("server/dist/bin.js")));
1226-
candidates.push(Some(resources.join("resources/server/dist/bin.js")));
1314+
let resources = dir.join("../Resources");
1315+
candidates.push(Some(resources.join("server/dist/bin.js")));
1316+
candidates.push(Some(resources.join("resources/server/dist/bin.js")));
12271317

1228-
let linux_resource_roots = [dir.join("../lib/CodeNomad"), dir.join("../lib/codenomad")];
1229-
for root in linux_resource_roots {
1230-
candidates.push(Some(root.join("server/dist/bin.js")));
1231-
candidates.push(Some(root.join("resources/server/dist/bin.js")));
1232-
}
1318+
let linux_resource_roots = [dir.join("../lib/CodeNomad"), dir.join("../lib/codenomad")];
1319+
for root in linux_resource_roots {
1320+
candidates.push(Some(root.join("server/dist/bin.js")));
1321+
candidates.push(Some(root.join("resources/server/dist/bin.js")));
12331322
}
12341323
}
12351324

1236-
let base = workspace_root();
1237-
candidates.push(
1238-
base.as_ref()
1239-
.map(|p| p.join("packages/server/dist/bin.js")),
1240-
);
1325+
candidates.push(workspace.map(|p| p.join("packages/server/dist/bin.js")));
12411326

1242-
first_existing(candidates)
1327+
candidates
12431328
}
12441329

12451330
fn build_shell_command_string(
@@ -1355,3 +1440,53 @@ fn normalize_path(path: PathBuf) -> String {
13551440
rendered
13561441
}
13571442
}
1443+
1444+
#[cfg(test)]
1445+
mod tests {
1446+
use super::*;
1447+
use std::sync::Mutex as StdMutex;
1448+
1449+
static ENV_LOCK: StdMutex<()> = StdMutex::new(());
1450+
1451+
#[test]
1452+
fn prod_entry_candidates_prefer_exe_relative_before_workspace_fallback() {
1453+
let exe_dir = PathBuf::from("/opt/codenomad/bin");
1454+
let workspace = PathBuf::from("/workspace/codenomad");
1455+
1456+
let candidates = prod_entry_candidates(Some(exe_dir.clone()), Some(workspace.clone()))
1457+
.into_iter()
1458+
.flatten()
1459+
.collect::<Vec<_>>();
1460+
1461+
assert_eq!(
1462+
candidates.first(),
1463+
Some(&exe_dir.join("resources/server/dist/bin.js"))
1464+
);
1465+
assert_eq!(
1466+
candidates.last(),
1467+
Some(&workspace.join("packages/server/dist/bin.js"))
1468+
);
1469+
}
1470+
1471+
#[test]
1472+
fn augment_launch_url_trims_leading_fragment_marker() {
1473+
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
1474+
std::env::set_var("CODENOMAD_UI_LAUNCH_QUERY", "#debug=true");
1475+
1476+
let augmented = augment_launch_url("http://127.0.0.1:3000");
1477+
1478+
std::env::remove_var("CODENOMAD_UI_LAUNCH_QUERY");
1479+
assert_eq!(augmented, "http://127.0.0.1:3000?debug=true");
1480+
}
1481+
1482+
#[test]
1483+
fn augment_launch_url_trims_fragment_marker_when_query_exists() {
1484+
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
1485+
std::env::set_var("CODENOMAD_UI_LAUNCH_QUERY", "#debug=true");
1486+
1487+
let augmented = augment_launch_url("http://127.0.0.1:3000?existing=true");
1488+
1489+
std::env::remove_var("CODENOMAD_UI_LAUNCH_QUERY");
1490+
assert_eq!(augmented, "http://127.0.0.1:3000?existing=true&debug=true");
1491+
}
1492+
}

0 commit comments

Comments
 (0)