← Commit history

verb_times and prewarm in the standard's shapes

Colby Knox ·bc0130c489 ·12d ago ·parent 6343078
4 files changed +39−18
CHANGELOG.md+4
@@ -5,6 +5,10 @@ Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/  ## 1.13.28 (unreleased) +- `fusion_verb_times` and `fusion_prewarm` answer the standard's shapes (standard 0.26.0, #29). verb_times: `verbs` is+  the list `[{verb, count, p50Sec, p90Sec, maxSec, lastSec, failures}]`, slowest p90 first, plus `since` and `limit`;+  the older map stays as `byVerb`. prewarm: `ready`, and each step's `done` (its `ok` kept).+ - Three standard verbs that were not_supported now work (eda-bridge-standard#29, every verb):   - `fusion_plate_vias`: each unplated `<hole>` in a .lbr's footprints becomes a plated pad. One inside an SMD land (a     heatsink via in an exposed pad) is named N@k after the land and joined to its connects, so it is on the land's
contracts/describe.json+12−4
@@ -20037,7 +20037,8 @@         "fusionInstalled": "bool",         "fusionStarted": "bool (always false)",         "ok": "bool",-        "steps": "[{step, ok, ...}]"+        "ready": "bool (the standard's prewarm)",+        "steps": "[{step, done, ok, skipped?, ...}]"       },       "statusVerb": null,       "summary": "Warm the bridge ahead of the first real call, WITHOUT starting Fusion: confirms the add-in is synced into every Fusion add-in folder, readies the space-free staging folder, and caches the PCB design-rules catalog from adom/pcb-design-rules. Adom Bridge calls it once per installed version in the background (bridge.json prewarm.verb); safe to call again any time. Launching Fusion is never part of it.",@@ -22967,12 +22968,19 @@       "timeoutSeconds": 120     },     {-      "example": {},-      "input": {},+      "example": {+        "limit": 20+      },+      "input": {+        "limit": "int, default 40: how many verbs"+      },       "longRunning": true,       "name": "fusion_verb_times",       "output": {-        "stages": "{name: {count, p50, p90}}"+        "byVerb": "{verb: {count, p50, p90, max, failures}} (the older map)",+        "count": "int",+        "since": "when the log starts (ISO 8601)",+        "verbs": "[{verb, count, p50Sec, p90Sec, maxSec, lastSec, failures}] slowest p90 first (the standard's verb_times)"       },       "statusVerb": null,       "summary": "This machine MEASURED timings per verb/stage - the same store that paces every progress bar and ETA. Read it to see whether an estimate is measured (this box has run it) or typical (still the seeded literal), and to explain a slow run.",
describe.py+2−2
@@ -692,7 +692,7 @@ _T = [     ("fusion_release_seat", "Give up the Autodesk seat from THIS machine so another box can claim it with no modal at all: closes Fusion cleanly (documents are NOT discarded; an unsaved one refuses and says so) and leaves the account signed in, so this box can claim the seat straight back. The counterpart of fusion_claim_seat.",      {"save": "optional bool (default false): save open documents before closing"}, {"success": "bool", "released": "bool", "wasHeldBy": "obj"}, 180, None, False, {}),     ("fusion_prewarm", "Warm the bridge ahead of the first real call, WITHOUT starting Fusion: confirms the add-in is synced into every Fusion add-in folder, readies the space-free staging folder, and caches the PCB design-rules catalog from adom/pcb-design-rules. Adom Bridge calls it once per installed version in the background (bridge.json prewarm.verb); safe to call again any time. Launching Fusion is never part of it.",-     {}, {"ok": "bool", "steps": "[{step, ok, ...}]", "fusionInstalled": "bool", "fusionStarted": "bool (always false)"}, 120, None, False, {}),+     {}, {"ready": "bool (the standard's prewarm)", "ok": "bool", "steps": "[{step, done, ok, skipped?, ...}]", "fusionInstalled": "bool", "fusionStarted": "bool (always false)"}, 120, None, False, {}),     ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.",      {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}), @@ -990,7 +990,7 @@ _T = [     ("fusion_take_screenshot", "Screenshot the Fusion window in the BACKGROUND (no focus steal, reads through occlusion). Use it to VERIFY your own work: geometry at the wrong Z, an empty view, or a Placeholder 3D body all render successfully and only a look at the pixels catches them.",      {"outputPath": "optional str"}, {"success": "bool", "fullPath": "str (auto-pulled by ab, 1.10.37)", "path": "str"}, 60, None, True, {}),     ("fusion_verb_times", "This machine MEASURED timings per verb/stage - the same store that paces every progress bar and ETA. Read it to see whether an estimate is measured (this box has run it) or typical (still the seeded literal), and to explain a slow run.",-     {}, {"stages": "{name: {count, p50, p90}}"}, 15, None, True, {}),+     {"limit": "int, default 40: how many verbs"}, {"verbs": "[{verb, count, p50Sec, p90Sec, maxSec, lastSec, failures}] slowest p90 first (the standard's verb_times)", "byVerb": "{verb: {count, p50, p90, max, failures}} (the older map)", "since": "when the log starts (ISO 8601)", "count": "int"}, 15, None, True, {"limit": 20}),     ("fusion_library_parts", "Every ORDERABLE variant in a .lbr with its part numbers - offline, no Fusion needed. For an EE this is the point of the library: two variants can share ONE symbol, ONE footprint and ONE 3D body and still be different products (an AEC-Q100 automotive part vs the baseline one), and that difference lives ENTIRELY in the metadata. EAGLE spells the orderable identity as deviceset + device + technology: <device> carries the PACKAGE variant, <technology> carries the GRADE/temperature variant, and attributes hang off <technology>. Reports per-variant MPN / MANUFACTURER / MOUSER_PN / DIGIKEY_PN / LCSC_PN (the exact names this bridge's own BOM export reads), plus warnings when variants are indistinguishable, share an MPN, or a deviceset has no description. These warnings never block the library from opening - they say whether an EE could actually order from it.",      {"libraryPath": "required str (also accepts lbrPath/filePath/path)"},      {"success": "bool", "devicesets": "[{name, description, variants:[{fullName, device, technology, package, attributes, missing}]}]", "totals": "{devicesets, variants, withMpn, withDistributor}", "metadataOk": "bool", "warnings": "[]"},
rust/bridge/src/main.rs+21−12
@@ -610,7 +610,9 @@ fn main() {                         } else { steps.push(json!({"step": "add-in sync", "ok": true, "skipped": "Fusion is not installed on this machine"})); }                         match fusionpath::stage_root(&sd) { Ok(p) => steps.push(json!({"step": "space-free staging folder ready", "ok": true, "path": p.to_string_lossy()})), Err(e) => { ok = false; steps.push(json!({"step": "staging folder", "ok": false, "error": e})); } }                         match services::design_rules_index(&sd) { Ok(i) => steps.push(json!({"step": "PCB design-rules catalog cached", "ok": true, "packs": i["vendors"].as_array().map(|a| a.len()).unwrap_or(0)})), Err(e) => steps.push(json!({"step": "PCB design-rules catalog", "ok": false, "error": e.chars().take(160).collect::<String>(), "note": "not fatal: fetched on first use"})) }-                        json!({"success": ok, "ok": ok, "shell": "rust", "fusionInstalled": fusion_installed, "fusionStarted": false, "steps": steps, "durationMs": t0.elapsed().as_millis() as u64,+                        // the standard's prewarm: each step's done (ok kept), and ready+                        for s in steps.iter_mut() { s["done"] = json!(s["ok"].as_bool() == Some(true) && s["skipped"].is_null()); }+                        json!({"success": ok, "ok": ok, "ready": ok, "mutated": false, "shell": "rust", "fusionInstalled": fusion_installed, "fusionStarted": false, "steps": steps, "durationMs": t0.elapsed().as_millis() as u64,                             "_hint": if fusion_installed { "Prewarmed without starting Fusion. The first fusion_start still pays Fusion's own launch; nothing a bridge can do ahead of time shortens that." } else { "Fusion is not installed here, so there is nothing to warm. fusion_readiness explains onboarding." }})                     },                     v @ ("fusion_check_dialogs" | "check_dialogs" | "fusion_dismiss_recovery" | "dismiss_recovery" | "fusion_relocate_recovery" | "relocate_recovery" | "fusion_close" | "close") if addin_owner_is_exe(&**h) => {@@ -881,31 +883,38 @@ fn pct(sorted: &[f64], q: f64) -> Option<f64> { fn native_verb_times(h: &dyn HostBoth, limit: usize) -> Value {     let p = h.bridge_state_dir().join("verb-times.jsonl");     let hint = "Aggregated from ~/.adom/fusion-bridge/verb-times.jsonl (every run, with its outcome). p90 is the useful column when hunting a slow surface; `failures` next to a healthy p50 usually means a verb that fails FAST, which is a different bug from one that is slow.";-    let mut runs: Vec<(String, Vec<f64>, u64)> = Vec::new(); // insertion order like a dict+    let mut runs: Vec<(String, Vec<f64>, u64, f64)> = Vec::new(); // insertion order like a dict; last run's seconds+    let mut since: Option<i64> = None;     if let Ok(text) = std::fs::read_to_string(&p) {         for line in text.lines() {             let Ok(r) = serde_json::from_str::<Value>(line) else { continue };+            if since.is_none() { since = r["at"].as_i64(); }             let verb = r["verb"].as_str().unwrap_or("?").to_string();             let secs = r["seconds"].as_f64().unwrap_or(0.0);             let ok = r["ok"].as_bool().unwrap_or(false);-            match runs.iter_mut().find(|(v, _, _)| *v == verb) {-                Some(e) => { e.1.push(secs); if !ok { e.2 += 1; } }-                None => runs.push((verb, vec![secs], if ok { 0 } else { 1 })),+            match runs.iter_mut().find(|(v, _, _, _)| *v == verb) {+                Some(e) => { e.1.push(secs); if !ok { e.2 += 1; } e.3 = secs; }+                None => runs.push((verb, vec![secs], if ok { 0 } else { 1 }, secs)),             }         }     }-    let mut rows: Vec<(String, Value, f64)> = runs.into_iter().map(|(verb, mut secs, fail)| {+    let mut rows: Vec<(String, Value, f64)> = runs.into_iter().map(|(verb, mut secs, fail, last)| {         secs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));         let p90 = pct(&secs, 0.9);-        let row = json!({"count": secs.len(), "p50": pct(&secs, 0.5), "p90": p90,-                         "max": secs.last().copied(), "failures": fail});+        let row = json!({"verb": verb, "count": secs.len(), "p50Sec": pct(&secs, 0.5), "p90Sec": p90, "maxSec": secs.last().copied(), "lastSec": last, "failures": fail});         (verb, row, p90.unwrap_or(0.0))     }).collect();     rows.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); // stable, like Python's sort-    let mut verbs = serde_json::Map::new();-    for (verb, row, _) in rows.into_iter().take(limit) { verbs.insert(verb, row); }-    let count = verbs.len();-    json!({"success": true, "verbs": verbs, "count": count, "shell": "rust", "_hint": hint})+    // the standard's verb_times: verbs is the list, slowest p90 first; byVerb keeps the older map+    let mut by = serde_json::Map::new();+    let mut list = Vec::new();+    for (verb, row, _) in rows.into_iter().take(limit) {+        by.insert(verb, json!({"count": row["count"], "p50": row["p50Sec"], "p90": row["p90Sec"], "max": row["maxSec"], "failures": row["failures"]}));+        list.push(row);+    }+    let count = list.len();+    let since = since.and_then(|s| chrono::DateTime::from_timestamp(s, 0)).map(|d| d.to_rfc3339());+    json!({"success": true, "mutated": false, "verbs": list, "byVerb": by, "since": since, "count": count, "shell": "rust", "_hint": hint}) }