skilldrop-cli 0.16.22 → 0.16.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packs/solution-architect/skills/design-doc/manifest.json +1 -1
- package/packs/solution-architect/skills/design-doc/templates/design-doc.md +1 -1
- package/packs/solution-architect/skills/interactive-diagram/SKILL.md +37 -9
- package/packs/solution-architect/skills/interactive-diagram/evals/evals.json +90 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/10/build-4127-timing.json +179 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/AbstractSensor.java +36 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/AlertService.java +15 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/Greenhouse.java +36 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/Sensor.java +10 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/SoilMoistureSensor.java +25 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/TemperatureSensor.java +19 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/11/src/org/example/greenhouse/Zone.java +26 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/6/notes.txt +7 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/6/trace-quote-slow.jsonl +13 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/7/oncall-notes.md +16 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/7/system-log.txt +14 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/8/run-4471.json +151 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/9/chat-export.txt +16 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/9/lesson-sync.log +24 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/9/ticket-8812.csv +6 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.html +25 -8
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-trace.html +203 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-trace.json +110 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/incident-timeline.html +203 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/incident-timeline.json +21 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/inputs/incident/oncall-notes.md +14 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/inputs/trace/checkout-trace.jsonl +9 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.html +25 -8
- package/packs/solution-architect/skills/interactive-diagram/manifest.json +2 -2
- package/packs/solution-architect/skills/interactive-diagram/reference.md +86 -6
- package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +1025 -102
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-theme-default="auto"><head><meta charset="utf-8">
|
|
3
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-KdTstAM29gVIP104X7Tju5/PTeQKbRDMMG1TjJdsOyI='; style-src 'unsafe-inline'; img-src data: blob:; base-uri 'none'; form-action 'none'">
|
|
4
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5
|
+
<!-- One self-contained file: no external scripts, styles, fonts or requests. The policy above makes the browser refuse any. -->
|
|
6
|
+
<title>Where a slow checkout spent its time</title><meta name="generator" content="interactive-diagram (skilldrop)"></head>
|
|
7
|
+
<body>
|
|
8
|
+
<header class="top"><h1>Where a slow checkout spent its time</h1><p class="q">Why did this checkout take 1.8 seconds?</p><p class="sum">Two thirds of it was the Stripe call, and most of that was waiting on Stripe, not the TLS handshake. A cache miss on prices cost another 350 ms before payment even started.</p><div class="findings"><b>What it shows</b><ul><li>The whole of POST /checkout took 1.8 s.</li><li>What the whole waited on most: stripe.PaymentIntent.create, 1.1 s of its own work (60%); select price from products (cache miss), 346 ms of its own work (19%); insert into orders, 165 ms of its own work (9%).</li><li>POST /checkout takes 1.8 s but only 27 ms of it is its own; the rest is waiting on what it calls.</li><li>price cart takes 380 ms but only 15 ms of it is its own; the rest is waiting on what it calls.</li></ul></div><p class="muted" style="font-size:12px;margin:6px 0 0">9 of 9 file citations checked against the repository</p></header>
|
|
9
|
+
<section class="text" id="outline" aria-label="The diagram as text">
|
|
10
|
+
<details open><summary>The diagram as text</summary><h2>Spans, in call order</h2><p>Total 1.8 s. What it waited on, in order: verify session → redis MGET price:* → select price from products (cache miss) → stripe.PaymentIntent.create → TLS handshake api.stripe.com → insert into orders → publish fulfilment.</p><ul><li><b>POST /checkout</b>: starts at 0 ms, takes 1.8 s (100%) <span class="ev">(<code>checkout-trace.jsonl:1</code> ✓)</span></li><li> <b>verify session</b>: starts at 4 ms, takes 34 ms (2%) <span class="ev">(<code>checkout-trace.jsonl:2</code> ✓)</span></li><li> <b>price cart</b>: starts at 40 ms, takes 380 ms (21%) <span class="ev">(<code>checkout-trace.jsonl:3</code> ✓)</span></li><li> <b>redis MGET price:*</b>: starts at 42 ms, takes 19 ms (1%) <span class="ev">(<code>checkout-trace.jsonl:4</code> ✓)</span></li><li> <b>select price from products (cache miss)</b>: starts at 64 ms, takes 346 ms (19%) <span class="ev">(<code>checkout-trace.jsonl:5</code> ✓)</span></li><li> <b>stripe.PaymentIntent.create</b>: starts at 430 ms, takes 1.2 s (65%) <span class="ev">(<code>checkout-trace.jsonl:6</code> ✓)</span></li><li> <b>TLS handshake api.stripe.com</b>: starts at 431 ms, takes 89 ms (5%) <span class="ev">(<code>checkout-trace.jsonl:7</code> ✓)</span></li><li> <b>insert into orders</b>: starts at 1.6 s, takes 165 ms (9%) <span class="ev">(<code>checkout-trace.jsonl:8</code> ✓)</span></li><li> <b>publish fulfilment</b>: starts at 1.8 s, takes 44 ms (2%) <span class="ev">(<code>checkout-trace.jsonl:9</code> ✓)</span></li></ul><h2>Notes</h2><ul><li>One trace, from checkout-trace.jsonl. It says where this request spent its time, not how often checkouts are this slow.</li></ul></details></section>
|
|
11
|
+
<nav class="bar" aria-label="Diagram controls">
|
|
12
|
+
<input id="find" type="search" placeholder="Find a part…" aria-label="Find a part">
|
|
13
|
+
<button id="zin" title="Zoom in">+</button><button id="zout" title="Zoom out">−</button><button id="zfit" title="Fit to screen">Fit</button>
|
|
14
|
+
<span class="sep"></span><button id="tour">Walk through</button><button id="prev" title="Previous step">←</button><button id="next" title="Next step">→</button>
|
|
15
|
+
<span class="sep"></span><button id="xsvg">SVG</button><button id="xpng">PNG</button><button id="xmmd" title="Copy and download Mermaid">Mermaid</button>
|
|
16
|
+
<button id="theme" title="Light or dark">◐</button>
|
|
17
|
+
<span class="hint">Click a part for detail and evidence · shift-click a second part for the path · drag to pan · scroll to zoom</span></nav>
|
|
18
|
+
<div class="legend"><span>▬ a span, to scale</span><span style="color:var(--hl)">▭ what the whole waited on (the critical path)</span><span style="color:var(--bad)">▭ failed</span><span>total 1.8 s</span></div>
|
|
19
|
+
<div class="caption" id="caption" aria-live="polite"></div>
|
|
20
|
+
<div class="wrap"><div id="stage"><svg id="map" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1381 352" width="1381" height="352" role="group" aria-label="Where a slow checkout spent its time"><defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="userSpaceOnUse" markerWidth="10" markerHeight="10" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" class="arrowhead"/></marker><marker id="arrow-start" viewBox="0 0 10 10" refX="1" refY="5" markerUnits="userSpaceOnUse" markerWidth="10" markerHeight="10" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" class="arrowhead"/></marker><marker id="tri" viewBox="0 0 12 12" refX="11" refY="6" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto-start-reverse"><path d="M1,1 L11,6 L1,11 z" class="mk-open"/></marker><marker id="open" viewBox="0 0 12 12" refX="11" refY="6" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto-start-reverse"><path d="M1,1 L11,6 L1,11" class="mk-line"/></marker><marker id="dia-solid" viewBox="0 0 16 10" refX="15" refY="5" markerUnits="userSpaceOnUse" markerWidth="18" markerHeight="11" orient="auto-start-reverse"><path d="M1,5 L8,1 L15,5 L8,9 z" class="arrowhead"/></marker><marker id="dia-open" viewBox="0 0 16 10" refX="15" refY="5" markerUnits="userSpaceOnUse" markerWidth="18" markerHeight="11" orient="auto-start-reverse"><path d="M1,5 L8,1 L15,5 L8,9 z" class="mk-open"/></marker></defs><rect class="canvas" x="0" y="0" width="1381" height="352"/><g class="axis"><line class="grid" x1="341.8" y1="44" x2="341.8" y2="316.0"/><text x="341.8" y="38" text-anchor="middle">0 ms</text><line class="grid" x1="581.0" y1="44" x2="581.0" y2="316.0"/><text x="581.0" y="38" text-anchor="middle">500 ms</text><line class="grid" x1="820.1" y1="44" x2="820.1" y2="316.0"/><text x="820.1" y="38" text-anchor="middle">1.0 s</text><line class="grid" x1="1059.2" y1="44" x2="1059.2" y2="316.0"/><text x="1059.2" y="38" text-anchor="middle">1.5 s</text></g><g class="node span crit" id="n-a1" data-id="a1" tabindex="0" role="button" aria-label="POST /checkout, 1.8 s"><title>POST /checkout — 1.8 s</title><rect class="rowbg" x="20" y="56.0" width="1341.8" height="26"/><text class="label wf" x="30.0" y="73.0">POST /checkout</text><rect class="shape bar" style="fill:#3b82f6" x="341.8" y="60.0" width="880.0" height="18.0" rx="3"/><text class="note wf" x="1227.8" y="73.0">1.8 s</text></g><g class="node span crit" id="n-a2" data-id="a2" tabindex="0" role="button" aria-label="verify session, 34 ms"><title>verify session — 34 ms</title><rect class="rowbg" x="20" y="84.0" width="1341.8" height="26"/><text class="label wf" x="48.0" y="101.0">verify session</text><rect class="shape bar" style="fill:#3b82f6" x="343.8" y="88.0" width="16.3" height="18.0" rx="3"/><text class="note wf" x="366.0" y="101.0">34 ms</text></g><g class="node span crit" id="n-a3" data-id="a3" tabindex="0" role="button" aria-label="price cart, 380 ms"><title>price cart — 380 ms</title><rect class="rowbg" x="20" y="112.0" width="1341.8" height="26"/><text class="label wf" x="48.0" y="129.0">price cart</text><rect class="shape bar" style="fill:#3b82f6" x="361.0" y="116.0" width="181.7" height="18.0" rx="3"/><text class="note wf" x="548.7" y="129.0">380 ms</text></g><g class="node span crit" id="n-a4" data-id="a4" tabindex="0" role="button" aria-label="redis MGET price:*, 19 ms"><title>redis MGET price:* — 19 ms</title><rect class="rowbg" x="20" y="140.0" width="1341.8" height="26"/><text class="label wf" x="66.0" y="157.0">redis MGET price:*</text><rect class="shape bar" style="fill:#16a34a" x="361.9" y="144.0" width="9.1" height="18.0" rx="3"/><text class="note wf" x="377.0" y="157.0">19 ms</text></g><g class="node span crit" id="n-a5" data-id="a5" tabindex="0" role="button" aria-label="select price from products (cache miss), 346 ms"><title>select price from products (cache miss) — 346 ms</title><rect class="rowbg" x="20" y="168.0" width="1341.8" height="26"/><text class="label wf" x="66.0" y="185.0">select price from products (cache miss)</text><rect class="shape bar" style="fill:#d97706" x="372.4" y="172.0" width="165.5" height="18.0" rx="3"/><text class="note wf" x="543.9" y="185.0">346 ms</text></g><g class="node span crit" id="n-a6" data-id="a6" tabindex="0" role="button" aria-label="stripe.PaymentIntent.create, 1.2 s"><title>stripe.PaymentIntent.create — 1.2 s</title><rect class="rowbg" x="20" y="196.0" width="1341.8" height="26"/><text class="label wf" x="48.0" y="213.0">stripe.PaymentIntent.create</text><rect class="shape bar" style="fill:#8b5cf6" x="547.5" y="200.0" width="569.1" height="18.0" rx="3"/><text class="note wf" x="1122.6" y="213.0">1.2 s</text></g><g class="node span crit" id="n-a7" data-id="a7" tabindex="0" role="button" aria-label="TLS handshake api.stripe.com, 89 ms"><title>TLS handshake api.stripe.com — 89 ms</title><rect class="rowbg" x="20" y="224.0" width="1341.8" height="26"/><text class="label wf" x="66.0" y="241.0">TLS handshake api.stripe.com</text><rect class="shape bar" style="fill:#8b5cf6" x="548.0" y="228.0" width="42.6" height="18.0" rx="3"/><text class="note wf" x="596.5" y="241.0">89 ms</text></g><g class="node span crit" id="n-a8" data-id="a8" tabindex="0" role="button" aria-label="insert into orders, 165 ms"><title>insert into orders — 165 ms</title><rect class="rowbg" x="20" y="252.0" width="1341.8" height="26"/><text class="label wf" x="48.0" y="269.0">insert into orders</text><rect class="shape bar" style="fill:#d97706" x="1119.0" y="256.0" width="78.9" height="18.0" rx="3"/><text class="note wf" x="1203.9" y="269.0">165 ms</text></g><g class="node span crit" id="n-a9" data-id="a9" tabindex="0" role="button" aria-label="publish fulfilment, 44 ms"><title>publish fulfilment — 44 ms</title><rect class="rowbg" x="20" y="280.0" width="1341.8" height="26"/><text class="label wf" x="48.0" y="297.0">publish fulfilment</text><rect class="shape bar" style="fill:#0d9488" x="1198.9" y="284.0" width="21.0" height="18.0" rx="3"/><text class="note wf" x="1225.9" y="297.0">44 ms</text></g></svg></div><aside id="panel" aria-live="polite"></aside></div>
|
|
21
|
+
<style id="diagram-css">
|
|
22
|
+
:root{--bg:#f7f8fa;--panel:#ffffff;--ink:#16181d;--muted:#5b6170;--line:#d5d9e0;--edge:#6b7385;--accent:#2563eb;
|
|
23
|
+
--warn:#b45309;--ok:#15803d;--bad:#b91c1c;--hl:#f59e0b;--canvas:#fbfcfd;--group:#eef1f6;--group2:#e6eaf2;--shadow:0 1px 2px rgba(0,0,0,.06)}
|
|
24
|
+
:root[data-theme=dark]{--bg:#0f1115;--panel:#171a21;--ink:#e7e9ee;--muted:#9aa1b2;--line:#2b303b;--edge:#8b93a7;--accent:#60a5fa;
|
|
25
|
+
--warn:#f59e0b;--ok:#4ade80;--bad:#f87171;--hl:#fbbf24;--canvas:#12151b;--group:#1a1e27;--group2:#20252f;--shadow:none}
|
|
26
|
+
*{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
27
|
+
body{display:flex;flex-direction:column}body>*{flex:none}section.text{order:10}
|
|
28
|
+
header.top{padding:18px 24px 10px;max-width:1400px}header.top h1{margin:0 0 4px;font-size:21px}
|
|
29
|
+
.q{color:var(--muted);margin:0 0 6px}.sum{margin:0;max-width:900px}
|
|
30
|
+
.bar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:8px 24px;position:sticky;top:0;background:var(--bg);z-index:5;border-bottom:1px solid var(--line)}
|
|
31
|
+
.bar button,.bar input{font:inherit;font-size:13px;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:6px;padding:4px 9px;cursor:pointer}
|
|
32
|
+
.bar input{cursor:text;width:180px}.bar button:hover{border-color:var(--accent)}.bar .sep{width:1px;height:20px;background:var(--line);margin:0 4px}
|
|
33
|
+
.bar .hint{color:var(--muted);font-size:12px;margin-left:auto}
|
|
34
|
+
.wrap{display:flex;gap:0;height:calc(100vh - 150px);min-height:420px;padding:10px 24px 0}
|
|
35
|
+
#stage{flex:1;position:relative;overflow:hidden;border:1px solid var(--line);border-radius:10px;background:var(--canvas);touch-action:none;cursor:grab}
|
|
36
|
+
#stage.drag{cursor:grabbing}#stage svg{position:absolute;left:0;top:0;width:100%;height:100%}
|
|
37
|
+
aside#panel{width:340px;margin-left:12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:auto;padding:14px 16px;display:none}
|
|
38
|
+
aside#panel.open{display:block}aside h3{margin:0 0 2px;font-size:16px}aside .kind{color:var(--muted);font-size:12px;margin-bottom:8px}
|
|
39
|
+
aside .close{float:right;border:0;background:none;color:var(--muted);font-size:18px;cursor:pointer}
|
|
40
|
+
aside pre{background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:8px;overflow:auto;font-size:12px;max-height:260px}
|
|
41
|
+
aside ul{padding-left:18px;margin:4px 0}aside a.jump{color:var(--accent);cursor:pointer;text-decoration:none}
|
|
42
|
+
aside .acts button{font:inherit;font-size:12px;margin:2px 4px 2px 0;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:5px;padding:3px 7px;cursor:pointer}
|
|
43
|
+
.caption{display:none;margin:8px 24px 0;padding:8px 12px;border-left:3px solid var(--hl);background:var(--panel);border-radius:4px}
|
|
44
|
+
.caption.on{display:block}
|
|
45
|
+
section.text{max-width:1100px;padding:10px 24px 40px}section.text h2{font-size:15px;margin:18px 0 4px}
|
|
46
|
+
.tag{font-size:11px;padding:0 5px;border-radius:4px;border:1px solid var(--line);margin-left:4px}.tag.warn{color:var(--warn);border-color:var(--warn)}
|
|
47
|
+
.tag.st-added{color:var(--ok);border-color:var(--ok)}.tag.st-removed{color:var(--bad);border-color:var(--bad)}.tag.st-changed{color:var(--accent);border-color:var(--accent)}
|
|
48
|
+
.muted{color:var(--muted)}.ev{color:var(--muted);font-size:12px}code{font-size:12px}
|
|
49
|
+
.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--muted);padding:6px 24px 0}
|
|
50
|
+
.legend i{display:inline-block;width:22px;height:0;border-top:2px solid var(--edge);vertical-align:middle;margin-right:5px}
|
|
51
|
+
.legend i.d{border-top-style:dashed}.legend i.a{border-top:2px dashed var(--warn)}
|
|
52
|
+
.findings{margin:10px 0 0;padding:8px 14px;border-left:3px solid var(--accent);background:var(--panel);border-radius:4px;max-width:1000px}
|
|
53
|
+
.findings ul{margin:4px 0 0;padding-left:18px}.findings li{margin:2px 0}
|
|
54
|
+
table.answers{border-collapse:collapse;margin:10px 0 0;max-width:1100px;font-size:13px}
|
|
55
|
+
table.answers th,table.answers td{border:1px solid var(--line);padding:5px 9px;text-align:left;vertical-align:top}
|
|
56
|
+
table.answers th{background:var(--group2);font-weight:600}table.answers td.ev{font-size:12px;color:var(--muted)}
|
|
57
|
+
.issues{margin:8px 24px 0;padding:8px 12px;border:1px solid var(--warn);border-radius:6px;color:var(--warn);font-size:13px}
|
|
58
|
+
/* diagram */
|
|
59
|
+
.canvas{fill:var(--canvas)}
|
|
60
|
+
.group rect{fill:var(--group);stroke:var(--line);stroke-width:1}.group.d2 rect{fill:var(--group2)}.group.d3 rect{fill:var(--group)}
|
|
61
|
+
.glabel{font-size:12px;font-weight:600;fill:var(--muted);letter-spacing:.02em}
|
|
62
|
+
.group.st-removed rect{stroke:var(--bad);stroke-dasharray:6 4;fill:none}
|
|
63
|
+
.node{cursor:pointer;outline:none}.node .shape{stroke-width:1.5;fill:var(--panel)}.node .shape.solid{fill:var(--ink);stroke:var(--ink)}
|
|
64
|
+
.node .shape-line{fill:none;stroke-width:1.2}.node .glyph{fill:var(--muted)}
|
|
65
|
+
.node .label{font-size:13px;font-weight:600;fill:var(--ink)}.node .note{font-size:11px;fill:var(--muted)}
|
|
66
|
+
.node .badge{font-size:10px;fill:var(--ok)}.node .badge.warn{fill:var(--warn)}
|
|
67
|
+
.table-head{fill:var(--group2)}.fname{font-size:12px;fill:var(--ink)}.ftype{font-size:11px;fill:var(--muted)}.fkey{font-size:9px;font-weight:700;fill:var(--warn)}
|
|
68
|
+
.c-blue .shape,.c-blue .shape-line{stroke:#3b82f6}.c-violet .shape{stroke:#8b5cf6}.c-green .shape,.c-green .shape-line{stroke:#16a34a}
|
|
69
|
+
.c-teal .shape,.c-teal .shape-line{stroke:#0d9488}.c-amber .shape,.c-amber .shape-line{stroke:#d97706}.c-slate .shape{stroke:#64748b}
|
|
70
|
+
.c-rose .shape{stroke:#e11d48}.c-indigo .shape{stroke:#4f46e5}.k-external .shape{stroke-dasharray:5 3}
|
|
71
|
+
.node.assumed .shape{stroke:var(--warn)!important;stroke-dasharray:6 4}
|
|
72
|
+
.node.st-added .shape{stroke:var(--ok)!important;stroke-width:2.5}.node.st-changed .shape{stroke:var(--accent)!important;stroke-width:2.5}
|
|
73
|
+
.node.st-removed{opacity:.55}.node.st-removed .shape{stroke:var(--bad)!important;stroke-dasharray:6 4}.node.st-removed .label{text-decoration:line-through}
|
|
74
|
+
.edge .line{fill:none;stroke:var(--edge);stroke-width:1.5}.edge .hit{fill:none;stroke:transparent;stroke-width:12;cursor:pointer}
|
|
75
|
+
.edge.s-dashed .line{stroke-dasharray:7 5}.edge.s-dotted .line{stroke-dasharray:2 4}.edge.emph .line{stroke-width:2.6}
|
|
76
|
+
.edge.assumed .line{stroke:var(--warn);stroke-dasharray:6 4}.edge.st-added .line{stroke:var(--ok);stroke-width:2.2}
|
|
77
|
+
.edge.st-removed .line{stroke:var(--bad);stroke-dasharray:6 4;opacity:.7}
|
|
78
|
+
.arrowhead{fill:var(--edge)}.lbg{fill:var(--canvas);opacity:.92}.elabel{font-size:11px;fill:var(--muted)}.card{font-size:11px;font-weight:700;fill:var(--muted)}
|
|
79
|
+
.lifeline{stroke:var(--line);stroke-width:1.2;stroke-dasharray:4 4}.seqno{font-size:10px;fill:var(--muted)}
|
|
80
|
+
.frame rect{fill:none;stroke:var(--muted);stroke-width:1}.frame .ftab{fill:var(--group2);stroke:var(--muted)}.frame text{font-size:11px;fill:var(--ink);font-weight:600}
|
|
81
|
+
svg.focus .node,svg.focus .edge,svg.focus .group{opacity:.18;transition:opacity .15s}
|
|
82
|
+
svg.focus .on,svg.focus .group.on{opacity:1}svg .node.on .shape{stroke-width:2.6}svg .edge.on .line{stroke:var(--hl);stroke-width:2.6}
|
|
83
|
+
svg .node.sel .shape{stroke:var(--hl)!important;stroke-width:3}svg .node:focus-visible .shape{stroke:var(--accent)!important;stroke-width:3}
|
|
84
|
+
@media (prefers-reduced-motion:no-preference){svg .edge.on.flow .line{stroke-dasharray:8 6;animation:flow 1s linear infinite}}
|
|
85
|
+
@keyframes flow{to{stroke-dashoffset:-28}}
|
|
86
|
+
.lane rect{fill:var(--group);stroke:var(--line)}.lane-line{stroke:var(--line);stroke-width:2}
|
|
87
|
+
.axis line{stroke:var(--muted);stroke-width:1}.axis text{font-size:11px;fill:var(--muted)}.axis .grid{stroke:var(--line);stroke-dasharray:2 4}
|
|
88
|
+
.brk{stroke:var(--muted);stroke-width:1.5;fill:none}.brk-label{font-size:10px;fill:var(--muted)}
|
|
89
|
+
.node.event .shape{stroke-width:2;fill:var(--panel)}.node.event .bar{fill:var(--group2);stroke-width:1.5}
|
|
90
|
+
.node.event .leader{stroke:var(--line);stroke-width:1}.node.event.approx .shape{stroke-dasharray:3 2}.node .label.tl{font-size:12px}.node .note.tl{font-size:10px}
|
|
91
|
+
.node.span .rowbg{fill:transparent}.node.span:hover .rowbg,.node.span.on .rowbg{fill:var(--group)}
|
|
92
|
+
.node.span .bar{stroke:none;opacity:.88}.node.span.crit .bar{stroke:var(--hl);stroke-width:2.5;opacity:1}
|
|
93
|
+
.node.span.err .bar{stroke:var(--bad);stroke-width:2.5}.node .label.wf{font-size:12px;font-weight:500}.node .note.wf{font-size:11px}
|
|
94
|
+
.node .label.italic{font-style:italic}.mk-open{fill:var(--canvas);stroke:var(--edge);stroke-width:1.2}.mk-line{fill:none;stroke:var(--edge);stroke-width:1.4}
|
|
95
|
+
.edge.tree .line{stroke:var(--line);stroke-width:1.5}
|
|
96
|
+
aside table.facts{border-collapse:collapse;font-size:13px;margin:6px 0}aside table.facts th{text-align:left;color:var(--muted);font-weight:400;padding:2px 10px 2px 0}
|
|
97
|
+
@media print{.bar,aside,.caption{display:none!important}.wrap{height:auto}#stage{border:0}#stage svg{position:static;height:auto}}
|
|
98
|
+
</style>
|
|
99
|
+
<script type="application/json" id="diagram-data">{"type": "waterfall", "slug": "where-a-slow-checkout-spent-its-time", "source_url": null, "story": [], "nodes": [{"id": "a1", "label": "POST /checkout", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 1, "end": 1, "quote": "POST /checkout", "verified": true, "snippet": "{\"span\": \"POST /checkout\", \"id\": \"a1\", \"parent\": null, \"service\": \"api\", \"start_ms\": 0, \"end_ms\": 1840}"}], "assumed": false, "status": null, "facts": [["starts at", "0 ms"], ["takes", "1.8 s"], ["share of the whole", "100%"], ["time of its own (not in a child span)", "27 ms"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a2", "label": "verify session", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 2, "end": 2, "quote": "verify session", "verified": true, "snippet": "{\"span\": \"verify session\", \"id\": \"a2\", \"parent\": \"a1\", \"service\": \"api\", \"start_ms\": 4, \"end_ms\": 38}"}], "assumed": false, "status": null, "facts": [["starts at", "4 ms"], ["takes", "34 ms"], ["share of the whole", "2%"], ["time of its own (not in a child span)", "34 ms"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a3", "label": "price cart", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 3, "end": 3, "quote": "price cart", "verified": true, "snippet": "{\"span\": \"price cart\", \"id\": \"a3\", \"parent\": \"a1\", \"service\": \"api\", \"start_ms\": 40, \"end_ms\": 420}"}], "assumed": false, "status": null, "facts": [["starts at", "40 ms"], ["takes", "380 ms"], ["share of the whole", "21%"], ["time of its own (not in a child span)", "15 ms"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a4", "label": "redis MGET price:*", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 4, "end": 4, "quote": "redis MGET price:*", "verified": true, "snippet": "{\"span\": \"redis MGET price:*\", \"id\": \"a4\", \"parent\": \"a3\", \"service\": \"redis\", \"start_ms\": 42, \"end_ms\": 61}"}], "assumed": false, "status": null, "facts": [["starts at", "42 ms"], ["takes", "19 ms"], ["share of the whole", "1%"], ["time of its own (not in a child span)", "19 ms"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a5", "label": "select price from products (cache miss)", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 5, "end": 5, "quote": "select price from products (cache miss)", "verified": true, "snippet": "{\"span\": \"select price from products (cache miss)\", \"id\": \"a5\", \"parent\": \"a3\", \"service\": \"postgres\", \"start_ms\": 64, \"end_ms\": 410}"}], "assumed": false, "status": null, "facts": [["starts at", "64 ms"], ["takes", "346 ms"], ["share of the whole", "19%"], ["time of its own (not in a child span)", "346 ms"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a6", "label": "stripe.PaymentIntent.create", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 6, "end": 6, "quote": "stripe.PaymentIntent.create", "verified": true, "snippet": "{\"span\": \"stripe.PaymentIntent.create\", \"id\": \"a6\", \"parent\": \"a1\", \"service\": \"stripe\", \"start_ms\": 430, \"end_ms\": 1620}"}], "assumed": false, "status": null, "facts": [["starts at", "430 ms"], ["takes", "1.2 s"], ["share of the whole", "65%"], ["time of its own (not in a child span)", "1.1 s"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a7", "label": "TLS handshake api.stripe.com", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 7, "end": 7, "quote": "TLS handshake api.stripe.com", "verified": true, "snippet": "{\"span\": \"TLS handshake api.stripe.com\", \"id\": \"a7\", \"parent\": \"a6\", \"service\": \"stripe\", \"start_ms\": 431, \"end_ms\": 520}"}], "assumed": false, "status": null, "facts": [["starts at", "431 ms"], ["takes", "89 ms"], ["share of the whole", "5%"], ["time of its own (not in a child span)", "89 ms"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a8", "label": "insert into orders", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 8, "end": 8, "quote": "insert into orders", "verified": true, "snippet": "{\"span\": \"insert into orders\", \"id\": \"a8\", \"parent\": \"a1\", \"service\": \"postgres\", \"start_ms\": 1625, \"end_ms\": 1790}"}], "assumed": false, "status": null, "facts": [["starts at", "1.6 s"], ["takes", "165 ms"], ["share of the whole", "9%"], ["time of its own (not in a child span)", "165 ms"], ["on the critical path", "yes"]], "group_label": null}, {"id": "a9", "label": "publish fulfilment", "kind": "span", "note": null, "detail": null, "fields": [], "evidence": [{"file": "checkout-trace.jsonl", "start": 9, "end": 9, "quote": "publish fulfilment", "verified": true, "snippet": "{\"span\": \"publish fulfilment\", \"id\": \"a9\", \"parent\": \"a1\", \"service\": \"rabbitmq\", \"start_ms\": 1792, \"end_ms\": 1836}"}], "assumed": false, "status": null, "facts": [["starts at", "1.8 s"], ["takes", "44 ms"], ["share of the whole", "2%"], ["time of its own (not in a child span)", "44 ms"], ["on the critical path", "yes"]], "group_label": null}], "edges": [{"id": "w-a2", "from": "a1", "to": "a2", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}, {"id": "w-a3", "from": "a1", "to": "a3", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}, {"id": "w-a4", "from": "a3", "to": "a4", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}, {"id": "w-a5", "from": "a3", "to": "a5", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}, {"id": "w-a6", "from": "a1", "to": "a6", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}, {"id": "w-a7", "from": "a6", "to": "a7", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}, {"id": "w-a8", "from": "a1", "to": "a8", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}, {"id": "w-a9", "from": "a1", "to": "a9", "label": "", "detail": null, "evidence": [], "assumed": false, "status": null}], "mermaid": "gantt\n title Where a slow checkout spent its time\n dateFormat x\n axisFormat %S.%L\n section api\n POST /checkout :crit, a1, 0, 1840\n verify session :crit, a2, 4, 38\n price cart :crit, a3, 40, 420\n section redis\n redis MGET price -* :crit, a4, 42, 61\n section postgres\n select price from products (cache miss) :crit, a5, 64, 410\n insert into orders :crit, a8, 1625, 1790\n section stripe\n stripe.PaymentIntent.create :crit, a6, 430, 1620\n TLS handshake api.stripe.com :crit, a7, 431, 520\n section rabbitmq\n publish fulfilment :crit, a9, 1792, 1836\n", "words": {"out": "Child spans", "in": "Parent span", "reach_out": "Everything it spawned", "reach_in": "What it ran under"}, "auto_steps": [{"text": "verify session: 34 ms, 2% of the whole, 34 ms of its own", "nodes": ["a2"]}, {"text": "redis MGET price:*: 19 ms, 1% of the whole, 19 ms of its own", "nodes": ["a4"]}, {"text": "select price from products (cache miss): 346 ms, 19% of the whole, 346 ms of its own", "nodes": ["a5"]}, {"text": "stripe.PaymentIntent.create: 1.2 s, 65% of the whole, 1.1 s of its own", "nodes": ["a6"]}, {"text": "TLS handshake api.stripe.com: 89 ms, 5% of the whole, 89 ms of its own", "nodes": ["a7"]}, {"text": "insert into orders: 165 ms, 9% of the whole, 165 ms of its own", "nodes": ["a8"]}, {"text": "publish fulfilment: 44 ms, 2% of the whole, 44 ms of its own", "nodes": ["a9"]}]}</script>
|
|
100
|
+
<script>
|
|
101
|
+
(function(){
|
|
102
|
+
var DATA=JSON.parse(document.getElementById('diagram-data').textContent);var WD=DATA.words;
|
|
103
|
+
var svg=document.getElementById('map'),stage=document.getElementById('stage'),panel=document.getElementById('panel');
|
|
104
|
+
var root=document.documentElement;
|
|
105
|
+
var pref=localStorage.getItem('idg-theme')||root.getAttribute('data-theme-default');
|
|
106
|
+
if(pref==='auto'||!pref){pref=window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}
|
|
107
|
+
root.setAttribute('data-theme',pref);
|
|
108
|
+
var W=+svg.getAttribute('width'),H=+svg.getAttribute('height');svg.removeAttribute('width');svg.removeAttribute('height');
|
|
109
|
+
var vb={x:0,y:0,w:W,h:H};
|
|
110
|
+
function apply(){svg.setAttribute('viewBox',vb.x+' '+vb.y+' '+vb.w+' '+vb.h)}
|
|
111
|
+
function fit(){var r=stage.getBoundingClientRect();var s=Math.max(W/r.width,H/r.height,0.6);vb.w=r.width*s;vb.h=r.height*s;vb.x=(W-vb.w)/2;vb.y=(H-vb.h)/2;apply()}
|
|
112
|
+
function zoom(f,cx,cy){var r=stage.getBoundingClientRect();if(cx==null){cx=r.width/2;cy=r.height/2}
|
|
113
|
+
var px=vb.x+cx/r.width*vb.w,py=vb.y+cy/r.height*vb.h;var nw=Math.min(Math.max(vb.w*f,120),W*6);var k=nw/vb.w;
|
|
114
|
+
vb.x=px-(px-vb.x)*k;vb.y=py-(py-vb.y)*k;vb.w=nw;vb.h=vb.h*k;apply()}
|
|
115
|
+
stage.addEventListener('wheel',function(e){e.preventDefault();var r=stage.getBoundingClientRect();zoom(e.deltaY>0?1.12:1/1.12,e.clientX-r.left,e.clientY-r.top)},{passive:false});
|
|
116
|
+
var drag=null;
|
|
117
|
+
stage.addEventListener('pointerdown',function(e){if(e.target.closest('.node')||e.target.closest('.edge'))return;drag={x:e.clientX,y:e.clientY,vx:vb.x,vy:vb.y};stage.classList.add('drag');stage.setPointerCapture(e.pointerId)});
|
|
118
|
+
stage.addEventListener('pointermove',function(e){if(!drag)return;var r=stage.getBoundingClientRect();vb.x=drag.vx-(e.clientX-drag.x)*vb.w/r.width;vb.y=drag.vy-(e.clientY-drag.y)*vb.h/r.height;apply()});
|
|
119
|
+
stage.addEventListener('pointerup',function(){drag=null;stage.classList.remove('drag')});
|
|
120
|
+
// Lookups are Maps, never plain objects: ids come from the spec and the URL, and a key such as
|
|
121
|
+
// __proto__ must stay a key.
|
|
122
|
+
var nodes=new Map(),edges=new Map(),outE=new Map(),inE=new Map();
|
|
123
|
+
DATA.nodes.forEach(function(n){nodes.set(n.id,n);outE.set(n.id,[]);inE.set(n.id,[])});
|
|
124
|
+
DATA.edges.forEach(function(e){edges.set(e.id,e);if(outE.has(e.from))outE.get(e.from).push(e.id);if(inE.has(e.to))inE.get(e.to).push(e.id)});
|
|
125
|
+
function el(kind,id){return document.getElementById(kind+'-'+id)}
|
|
126
|
+
function clear(){svg.classList.remove('focus');svg.querySelectorAll('.on,.sel,.flow').forEach(function(x){x.classList.remove('on','sel','flow')})}
|
|
127
|
+
function light(ns,es,flow){clear();svg.classList.add('focus');ns.forEach(function(i){var x=el('n',i);x&&x.classList.add('on')});
|
|
128
|
+
es.forEach(function(i){var x=el('e',i);if(x){x.classList.add('on');flow&&x.classList.add('flow')}})}
|
|
129
|
+
function reach(id,dir){if(!nodes.has(id))return {n:[],e:[]};var seen=new Set([id]),es=[],q=[id];while(q.length){var u=q.shift();(dir>0?outE.get(u):inE.get(u)).forEach(function(eid){var e=edges.get(eid);es.push(eid);var v=dir>0?e.to:e.from;if(!seen.has(v)){seen.add(v);q.push(v)}})}
|
|
130
|
+
return {n:Array.from(seen),e:es}}
|
|
131
|
+
function path(a,b,directed){if(!nodes.has(a)||!nodes.has(b))return null;var prev=new Map([[a,null]]),q=[a];while(q.length){var u=q.shift();if(u===b)break;
|
|
132
|
+
var nb=outE.get(u).map(function(i){return [edges.get(i).to,i]});if(!directed)nb=nb.concat(inE.get(u).map(function(i){return [edges.get(i).from,i]}));
|
|
133
|
+
nb.forEach(function(p){if(!prev.has(p[0])){prev.set(p[0],[u,p[1]]);q.push(p[0])}})}
|
|
134
|
+
if(!prev.has(b))return null;var ns=[b],es=[],c=b;while(prev.get(c)){es.push(prev.get(c)[1]);c=prev.get(c)[0];ns.push(c)}return {n:ns.reverse(),e:es.reverse()}}
|
|
135
|
+
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
|
136
|
+
function evHtml(list){if(!list||!list.length)return '<p class="muted">No evidence cited.</p>';
|
|
137
|
+
return '<ul>'+list.map(function(e){var t=e.said!=null?(esc(e.source||'user')+': “'+esc(e.said)+'”'):esc(e.file+(e.start?':'+e.start+(e.end!==e.start?'-'+e.end:''):''));
|
|
138
|
+
if(e.file&&DATA.source_url){t='<a href="'+esc(DATA.source_url.replace(/\/?$/,'/')+e.file+(e.start?'#L'+e.start+(e.end!==e.start?'-L'+e.end:''):''))+'" target="_blank" rel="noopener">'+t+'</a>'}
|
|
139
|
+
var v=e.verified===true?' <span class="tag" style="color:var(--ok)">checked</span>':(e.verified===null&&e.file?' <span class="tag">not checked</span>':'');
|
|
140
|
+
return '<li>'+t+v+(e.snippet?'<pre>'+esc(e.snippet)+'</pre>':'')+'</li>'}).join('')+'</ul>'}
|
|
141
|
+
var selected=null;
|
|
142
|
+
function show(kind,id){var x=kind==='n'?nodes.get(id):edges.get(id);if(!x)return;var h='<button class="close" aria-label="Close">×</button>';
|
|
143
|
+
if(kind==='n'){h+='<h3>'+esc(x.label)+'</h3><div class="kind">'+esc(x.kind)+(x.group_label?' · '+esc(x.group_label):'')+(x.assumed?' · <span style="color:var(--warn)">assumption</span>':'')+(x.status?' · '+esc(x.status):'')+'</div>';
|
|
144
|
+
if(x.note)h+='<p>'+esc(x.note)+'</p>';if(x.detail)h+='<p>'+esc(x.detail)+'</p>';
|
|
145
|
+
if(x.fields&&x.fields.length)h+='<ul>'+x.fields.map(function(f){return '<li><code>'+esc(f.name)+'</code> '+esc(f.type||'')+(f.key?' <b>'+esc([].concat(f.key).join('/').toUpperCase())+'</b>':'')+(f.references?' → '+esc(f.references):'')+(f.note?' — '+esc(f.note):'')+'</li>'}).join('')+'</ul>';
|
|
146
|
+
if(x.facts&&x.facts.length)h+='<table class="facts">'+x.facts.map(function(f){return '<tr><th>'+esc(f[0])+'</th><td>'+esc(f[1])+'</td></tr>'}).join('')+'</table>';
|
|
147
|
+
h+='<div class="acts"><button data-act="out">'+esc(WD.reach_out)+'</button><button data-act="in">'+esc(WD.reach_in)+'</button><button data-act="near">Neighbours</button></div>';
|
|
148
|
+
h+='<p class="muted" style="font-size:12px">Shift-click another part to see the path between them.</p>';
|
|
149
|
+
var o=outE.get(id).map(function(i){var e=edges.get(i);return '<li><a class="jump" data-n="'+esc(e.to)+'">'+esc(nodes.get(e.to).label)+'</a>'+(e.label?' — '+esc(e.label):'')+'</li>'}).join('');
|
|
150
|
+
var n=inE.get(id).map(function(i){var e=edges.get(i);return '<li><a class="jump" data-n="'+esc(e.from)+'">'+esc(nodes.get(e.from).label)+'</a>'+(e.label?' — '+esc(e.label):'')+'</li>'}).join('');
|
|
151
|
+
if(o)h+='<b>'+esc(WD.out)+'</b><ul>'+o+'</ul>';if(n)h+='<b>'+esc(WD.in)+'</b><ul>'+n+'</ul>';
|
|
152
|
+
h+='<b>Evidence</b>'+evHtml(x.evidence);
|
|
153
|
+
var nb=new Set([id]);outE.get(id).concat(inE.get(id)).forEach(function(i){nb.add(edges.get(i).from);nb.add(edges.get(i).to)});light(Array.from(nb),outE.get(id).concat(inE.get(id)));el('n',id).classList.add('sel');
|
|
154
|
+
}else{h+='<h3>'+esc(nodes.get(x.from).label)+' → '+esc(nodes.get(x.to).label)+'</h3><div class="kind">'+(x.label?esc(x.label):'connection')+(x.assumed?' · <span style="color:var(--warn)">assumption</span>':'')+(x.status?' · '+esc(x.status):'')+'</div>';
|
|
155
|
+
if(x.detail)h+='<p>'+esc(x.detail)+'</p>';h+='<b>Evidence</b>'+evHtml(x.evidence);light([x.from,x.to],[id],true)}
|
|
156
|
+
panel.innerHTML=h;panel.classList.add('open');selected=kind==='n'?id:null;
|
|
157
|
+
panel.querySelector('.close').onclick=function(){panel.classList.remove('open');clear();selected=null};
|
|
158
|
+
panel.querySelectorAll('.jump').forEach(function(a){a.onclick=function(){show('n',a.getAttribute('data-n'))}});
|
|
159
|
+
panel.querySelectorAll('[data-act]').forEach(function(b){b.onclick=function(){var a=b.getAttribute('data-act');
|
|
160
|
+
if(a==='near'){show('n',id);return}var r=reach(id,a==='out'?1:-1);light(r.n,r.e,true);el('n',id).classList.add('sel')}})}
|
|
161
|
+
svg.addEventListener('click',function(e){var g=e.target.closest('.node');if(g){var id=g.getAttribute('data-id');
|
|
162
|
+
if(e.shiftKey&&selected&&selected!==id){var p=path(selected,id,true)||path(selected,id,false);if(p){light(p.n,p.e,true)}return}show('n',id);return}
|
|
163
|
+
var ed=e.target.closest('.edge');if(ed){show('e',ed.getAttribute('data-id'));return}});
|
|
164
|
+
svg.addEventListener('keydown',function(e){var g=e.target.closest&&e.target.closest('.node');if(g&&(e.key==='Enter'||e.key===' ')){e.preventDefault();show('n',g.getAttribute('data-id'))}});
|
|
165
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'){panel.classList.remove('open');clear();selected=null}
|
|
166
|
+
if(e.target.tagName==='INPUT')return;if(e.key==='ArrowRight'&&tour)step(1);if(e.key==='ArrowLeft'&&tour)step(-1)});
|
|
167
|
+
var q=document.getElementById('find');q.addEventListener('input',function(){var v=q.value.trim().toLowerCase();if(!v){clear();return}
|
|
168
|
+
var ns=DATA.nodes.filter(function(n){return (n.label+' '+(n.note||'')+' '+(n.fields||[]).map(function(f){return f.name}).join(' ')).toLowerCase().indexOf(v)>=0}).map(function(n){return n.id});light(ns,[])});
|
|
169
|
+
document.getElementById('zin').onclick=function(){zoom(1/1.25)};document.getElementById('zout').onclick=function(){zoom(1.25)};document.getElementById('zfit').onclick=fit;
|
|
170
|
+
document.getElementById('theme').onclick=function(){var t=root.getAttribute('data-theme')==='dark'?'light':'dark';root.setAttribute('data-theme',t);try{localStorage.setItem('idg-theme',t)}catch(e){}};
|
|
171
|
+
function download(name,type,data){var a=document.createElement('a');a.href=URL.createObjectURL(new Blob([data],{type:type}));a.download=name;document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(a.href);a.remove()},500)}
|
|
172
|
+
function standalone(){var c=svg.cloneNode(true);c.setAttribute('viewBox','0 0 '+W+' '+H);c.setAttribute('width',W);c.setAttribute('height',H);c.classList.remove('focus');
|
|
173
|
+
c.querySelectorAll('.on,.sel,.flow').forEach(function(x){x.classList.remove('on','sel','flow')});
|
|
174
|
+
var cs=getComputedStyle(root),vars=['--bg','--panel','--ink','--muted','--line','--edge','--accent','--warn','--ok','--bad','--hl','--canvas','--group','--group2'].map(function(v){return v+':'+cs.getPropertyValue(v)}).join(';');
|
|
175
|
+
var st=document.createElementNS('http://www.w3.org/2000/svg','style');st.textContent='svg{'+vars+';font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}'+document.getElementById('diagram-css').textContent.split('/* diagram */')[1];
|
|
176
|
+
c.insertBefore(st,c.firstChild);return new XMLSerializer().serializeToString(c)}
|
|
177
|
+
var base=(DATA.slug||'diagram');window.interactiveDiagram={svg:standalone,mermaid:function(){return DATA.mermaid}};
|
|
178
|
+
document.getElementById('xsvg').onclick=function(){download(base+'.svg','image/svg+xml',standalone())};
|
|
179
|
+
document.getElementById('xpng').onclick=function(){var img=new Image();img.onload=function(){var k=2,cv=document.createElement('canvas');cv.width=W*k;cv.height=H*k;var ctx=cv.getContext('2d');ctx.scale(k,k);ctx.drawImage(img,0,0);cv.toBlob(function(b){download(base+'.png','image/png',b)})};
|
|
180
|
+
img.src='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(standalone())};
|
|
181
|
+
document.getElementById('xmmd').onclick=function(){var t=DATA.mermaid;if(navigator.clipboard)navigator.clipboard.writeText(t).catch(function(){});download(base+'.mmd','text/plain',t)};
|
|
182
|
+
var tour=null,cap=document.getElementById('caption');
|
|
183
|
+
var steps=DATA.story&&DATA.story.length?DATA.story:(DATA.auto_steps&&DATA.auto_steps.length)?DATA.auto_steps:(DATA.type==='sequence'?DATA.edges.map(function(m,i){return {text:(i+1)+'. '+nodes.get(m.from).label+' → '+nodes.get(m.to).label+(m.label?': '+m.label:''),edges:[m.id],nodes:[m.from,m.to]}}):[]);
|
|
184
|
+
var tb=document.getElementById('tour');if(!steps.length){tb.style.display='none'}
|
|
185
|
+
function step(d){tour.i=Math.max(0,Math.min(steps.length-1,tour.i+d));var s=steps[tour.i];
|
|
186
|
+
light((s.nodes||[]).concat((s.edges||[]).map(function(i){return edges.get(i)?edges.get(i).from:null}).filter(Boolean)).concat((s.edges||[]).map(function(i){return edges.get(i)?edges.get(i).to:null}).filter(Boolean)),s.edges||[],true);
|
|
187
|
+
cap.innerHTML='<b>Step '+(tour.i+1)+' of '+steps.length+'</b> — '+esc(s.text||'')+' <span class="muted">(← → to move, Esc to stop)</span>';cap.classList.add('on')}
|
|
188
|
+
tb.onclick=function(){if(tour){tour=null;cap.classList.remove('on');clear();tb.textContent='Walk through';return}tour={i:-1};tb.textContent='Stop';step(1)};
|
|
189
|
+
document.getElementById('prev').onclick=function(){if(tour)step(-1)};document.getElementById('next').onclick=function(){if(!tour){tb.onclick()}else step(1)};
|
|
190
|
+
if(!steps.length){document.getElementById('prev').style.display='none';document.getElementById('next').style.display='none'}
|
|
191
|
+
function size(){var wr=document.querySelector('.wrap');var avail=Math.max(window.innerHeight-150,360);var r=stage.getBoundingClientRect();
|
|
192
|
+
var s=Math.max(W/Math.max(r.width,1),0.6);wr.style.height=Math.min(avail,Math.max(H/s+24,300))+'px'}
|
|
193
|
+
size();fit();window.addEventListener('resize',function(){size();fit()});
|
|
194
|
+
function hash(){var h=decodeURIComponent(location.hash.slice(1));if(!h)return;var kv=new Map();h.split('&').forEach(function(p){var i=p.indexOf('=');if(i>0)kv.set(p.slice(0,i),p.slice(i+1))});
|
|
195
|
+
if(kv.get('theme'))root.setAttribute('data-theme',kv.get('theme'));
|
|
196
|
+
if(kv.get('n')&&nodes.get(kv.get('n')))show('n',kv.get('n'));if(kv.get('e')&&edges.get(kv.get('e')))show('e',kv.get('e'));
|
|
197
|
+
if(kv.get('path')){var ab=kv.get('path').split(',');var p=path(ab[0],ab[1],true)||path(ab[0],ab[1],false);if(p)light(p.n,p.e,true)}
|
|
198
|
+
if(kv.get('reach')){var ar=kv.get('reach').split(',');var r=reach(ar[0],ar[1]==='up'?-1:1);light(r.n,r.e,true)}
|
|
199
|
+
if(kv.get('step')&&steps.length){tour={i:-1};tb.textContent='Stop';step(+kv.get('step'))}}
|
|
200
|
+
hash();window.addEventListener('hashchange',hash);
|
|
201
|
+
})();
|
|
202
|
+
</script>
|
|
203
|
+
</body></html>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "waterfall",
|
|
3
|
+
"title": "Where a slow checkout spent its time",
|
|
4
|
+
"unit": "ms",
|
|
5
|
+
"question": "Why did this checkout take 1.8 seconds?",
|
|
6
|
+
"summary": "Two thirds of it was the Stripe call, and most of that was waiting on Stripe, not the TLS handshake. A cache miss on prices cost another 350 ms before payment even started.",
|
|
7
|
+
"spans": [
|
|
8
|
+
{
|
|
9
|
+
"id": "a1",
|
|
10
|
+
"label": "POST /checkout",
|
|
11
|
+
"start": 0,
|
|
12
|
+
"end": 1840,
|
|
13
|
+
"service": "api",
|
|
14
|
+
"evidence": [
|
|
15
|
+
"checkout-trace.jsonl:1#POST /checkout"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "a2",
|
|
20
|
+
"label": "verify session",
|
|
21
|
+
"start": 4,
|
|
22
|
+
"end": 38,
|
|
23
|
+
"service": "api",
|
|
24
|
+
"parent": "a1",
|
|
25
|
+
"evidence": [
|
|
26
|
+
"checkout-trace.jsonl:2#verify session"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "a3",
|
|
31
|
+
"label": "price cart",
|
|
32
|
+
"start": 40,
|
|
33
|
+
"end": 420,
|
|
34
|
+
"service": "api",
|
|
35
|
+
"parent": "a1",
|
|
36
|
+
"evidence": [
|
|
37
|
+
"checkout-trace.jsonl:3#price cart"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "a4",
|
|
42
|
+
"label": "redis MGET price:*",
|
|
43
|
+
"start": 42,
|
|
44
|
+
"end": 61,
|
|
45
|
+
"service": "redis",
|
|
46
|
+
"parent": "a3",
|
|
47
|
+
"evidence": [
|
|
48
|
+
"checkout-trace.jsonl:4#redis MGET price:*"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "a5",
|
|
53
|
+
"label": "select price from products (cache miss)",
|
|
54
|
+
"start": 64,
|
|
55
|
+
"end": 410,
|
|
56
|
+
"service": "postgres",
|
|
57
|
+
"parent": "a3",
|
|
58
|
+
"evidence": [
|
|
59
|
+
"checkout-trace.jsonl:5#select price from products (cache miss)"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "a6",
|
|
64
|
+
"label": "stripe.PaymentIntent.create",
|
|
65
|
+
"start": 430,
|
|
66
|
+
"end": 1620,
|
|
67
|
+
"service": "stripe",
|
|
68
|
+
"parent": "a1",
|
|
69
|
+
"evidence": [
|
|
70
|
+
"checkout-trace.jsonl:6#stripe.PaymentIntent.create"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "a7",
|
|
75
|
+
"label": "TLS handshake api.stripe.com",
|
|
76
|
+
"start": 431,
|
|
77
|
+
"end": 520,
|
|
78
|
+
"service": "stripe",
|
|
79
|
+
"parent": "a6",
|
|
80
|
+
"evidence": [
|
|
81
|
+
"checkout-trace.jsonl:7#TLS handshake api.stripe.com"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "a8",
|
|
86
|
+
"label": "insert into orders",
|
|
87
|
+
"start": 1625,
|
|
88
|
+
"end": 1790,
|
|
89
|
+
"service": "postgres",
|
|
90
|
+
"parent": "a1",
|
|
91
|
+
"evidence": [
|
|
92
|
+
"checkout-trace.jsonl:8#insert into orders"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "a9",
|
|
97
|
+
"label": "publish fulfilment",
|
|
98
|
+
"start": 1792,
|
|
99
|
+
"end": 1836,
|
|
100
|
+
"service": "rabbitmq",
|
|
101
|
+
"parent": "a1",
|
|
102
|
+
"evidence": [
|
|
103
|
+
"checkout-trace.jsonl:9#publish fulfilment"
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"notes": [
|
|
108
|
+
"One trace, from checkout-trace.jsonl. It says where this request spent its time, not how often checkouts are this slow."
|
|
109
|
+
]
|
|
110
|
+
}
|