← Commit history

v2.0.693 (insiders): Annotate starts fast: pup's extension now holds a long poll that pup answers the moment it has a command, instead of checking every 1.2 s. Every command on the extension lane is picked up at once; Annotate's two steps took 1.5 to 2.2 s before you could draw (John, 2026-10-04). Measured: Annotate start 121 ms (was 0.6 to 2.3 s), pup_eval 121 ms (was 0.7 to 2.3 s).

John Lauer ·66e9958880 ·17d ago ·parent 32ca009
5 files changed +45−18
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.692"+version = "2.0.693" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.692"+version = "2.0.693" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.692"+version = "2.0.693" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.692"+version = "2.0.693" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.692"+version = "2.0.693" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.692"+version = "2.0.693" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.692"+version = "2.0.693" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.692"+version = "2.0.693" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.692"+version = "2.0.693" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.692"+version = "2.0.693" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.692"+version = "2.0.693" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.692"+version = "2.0.693" dependencies = [  "base64",  "png 0.17.16",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session",  # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.692"+version = "2.0.693" edition = "2021" license = "MIT" 
rust/bridge/src/main.rs+2−1
@@ -143,7 +143,8 @@ fn main() {                     let q = url.split('?').nth(1).unwrap_or("");                     let lane = q.split('&').find_map(|kv| kv.strip_prefix("lane=")).unwrap_or("").to_string();                     let removed: Vec<i64> = q.split('&').find_map(|kv| kv.strip_prefix("removed=")).unwrap_or("").split(',').filter_map(|x| x.parse().ok()).collect();-                    let _ = req.respond(json_response(200, &pup_verbs::extdrive::take_command_for(&lane, &removed)));+                    let wait: u64 = q.split('&').find_map(|kv| kv.strip_prefix("wait=")).and_then(|x| x.parse().ok()).unwrap_or(0);+                    let _ = req.respond(json_response(200, &pup_verbs::extdrive::take_command_for(&lane, &removed, wait)));                 }                 // 2.0.602: the extension lane's login-capture rail. The CDP lane got captures through a                 // DevTools binding; the default lane had none, so logins typed in normal pup windows were lost.
rust/verbs/src/extdrive.rs+18−1
@@ -83,13 +83,29 @@ static SEQ: Mutex<u64> = Mutex::new(0); pub fn take_removed() -> Vec<i64> { std::mem::take(&mut *st().removed.lock().unwrap()) }  /// GET /extcmd — the extension asks for work. Returns `{}` when there is nothing to do.-pub fn take_command_for(lane: &str, removed: &[i64]) -> Value {+pub fn take_command_for(lane: &str, removed: &[i64], wait_ms: u64) -> Value {     // 2.0.502: only a browser carrying a lane token pup issued may take work. 2.0.659: the token names WHICH     // lane (default adom-you, or a per-profile lane), so several browsers can be driven at once.     let Some(l) = lane_for_token(lane) else { return json!({}) };     if !removed.is_empty() { l.removed.lock().unwrap().extend_from_slice(removed); }+    let cmd = take_command_on(&l);+    if cmd["id"].is_u64() || wait_ms == 0 { return cmd; }+    // 2.0.693: a LONG poll. The worker used to ask every 1.2 s, so every command waited up to 1.2 s just to+    // be picked up, and a two-step verb like Annotate (inject the overlay, then start it) took 1.5 to 2.2 s+    // before the user could draw (John, 2026-10-04). Now the request waits here and run() wakes it the moment+    // a command is queued. Capped well under the MV3 worker's 30 s idle limit and alive()'s 20 s heartbeat.+    let deadline = Instant::now() + Duration::from_millis(wait_ms.min(10_000));+    let mut g = QUEUED.0.lock().unwrap();+    while l.pending.lock().unwrap().is_empty() {+        let now = Instant::now();+        if now >= deadline { break; }+        g = QUEUED.1.wait_timeout(g, deadline - now).unwrap().0;+    }+    drop(g);     take_command_on(&l) }+/// Rung by run() whenever a command is queued, so a waiting /extcmd long poll returns at once.+static QUEUED: (Mutex<()>, std::sync::Condvar) = (Mutex::new(()), std::sync::Condvar::new());  pub fn take_command() -> Value { take_command_on(&st()) } fn take_command_on(l: &LaneState) -> Value {@@ -310,6 +326,7 @@ fn run_inner(cmd: Value, wait: Duration) -> Result<Value, Value> {     let mut c = cmd;     c["id"] = json!(id);     st().pending.lock().unwrap().push_back(c);+    { let _g = QUEUED.0.lock().unwrap(); QUEUED.1.notify_all(); }     let t0 = Instant::now();     while t0.elapsed() < wait {         let hit = st().results.lock().unwrap().as_mut().and_then(|m| m.remove(&id));
src/extension/worker.js+12−3
@@ -256,11 +256,14 @@ async function pupExtPollOnce() {     const removed = pupRemovedWindows.splice(0, pupRemovedWindows.length);     const rq = removed.length ? `&removed=${removed.join(',')}` : '';     let r;-    try { r = await fetch(`http://127.0.0.1:${found.port}/extcmd?lane=${encodeURIComponent(_pkgLane)}${rq}`, { signal: AbortSignal.timeout(2500) }); }+    // 2.0.693: a long poll. pup holds this request until it has a command (up to 8 s) and answers the moment+    // one is queued, so a command no longer waits for the next 1.2 s tick (Annotate took 1.5 to 2.2 s).+    pupExtLastAsk = Date.now();+    try { r = await fetch(`http://127.0.0.1:${found.port}/extcmd?lane=${encodeURIComponent(_pkgLane)}${rq}&wait=8000`, { signal: AbortSignal.timeout(12000) }); }     catch (e) { pupRemovedWindows.unshift(...removed); throw e; }     cmd = await r.json();   } catch (e) { return false; }-  if (!cmd || !cmd.id) return false;+  if (!cmd || !cmd.id) return 'empty';      let out = { id: cmd.id, ok: false, error: `unsupported action ${cmd.action}` };   // every command races a deadline: a hung Chrome API answers as an error and names itself, instead of@@ -558,15 +561,21 @@ async function pupExtPollOnce() { // bridge saw total silence and declared the lane dead. Three rules: every command has a deadline, the busy // guard cannot stick (a drain older than 60 s is abandoned), and the hello has its own alarm below. let pupExtBusySince = 0;+let pupExtLastAsk = 0;+// 2.0.693: the drain keeps a long poll open back to back, so pup can hand over a command the moment it has+// one. The 1.2 s interval below only restarts the loop if it ever stops. An empty answer that came back+// fast means an older pup that does not hold the request: pause like the old tick so this cannot spin. async function pupExtPoll() {   if (pupExtBusy && Date.now() - pupExtBusySince < 60000) return;   if (pupExtBusy) { console.warn('[pup] a drain has been stuck for 60 s; starting a fresh one'); }   pupExtBusy = true; pupExtBusySince = Date.now();   try {-    for (let k = 0; k < 12; k++) {+    for (;;) {+      pupExtBusySince = Date.now();       const step = await Promise.race([pupExtPollOnce(), new Promise((res) => setTimeout(() => res('deadline'), 50000))]);       if (step === 'deadline') { console.warn('[pup] a poll step did not settle within 50 s; abandoning it'); break; }       if (!step) break;+      if (step === 'empty' && Date.now() - pupExtLastAsk < 1000) break;     }   }   finally { pupExtBusy = false; }