skilldrop-cli 0.16.21 → 0.16.23
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/bin/skilldrop.js +5 -3
- package/catalogue.json +1 -0
- package/model-routing.json +4 -0
- package/package.json +1 -1
- package/packs/solution-architect/skills/interactive-diagram/SKILL.md +160 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/eval_queries.json +12 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/evals.json +164 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/README.md +11 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/api/app.py +43 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/docker-compose.yml +20 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/web/checkout.js +9 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/worker/worker.py +23 -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/3/schema.sql +35 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/README.md +26 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/config/default.json +29 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/clients/transcription.js +25 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/db.js +16 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/handlers/storageWebhook.js +43 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/jobs/transcribeEpisode.js +28 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/mailer.js +18 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/server.js +32 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/worker.js +32 -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 +203 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.json +48 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.md +31 -0
- 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/checkout/api/checkout.py +41 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/docker-compose.yml +20 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/worker/fulfil.py +23 -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 +203 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.json +28 -0
- package/packs/solution-architect/skills/interactive-diagram/manifest.json +39 -0
- package/packs/solution-architect/skills/interactive-diagram/reference.md +231 -0
- package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +2605 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "architecture",
|
|
3
|
+
"title": "What a checkout request touches",
|
|
4
|
+
"question": "What happens between a shopper pressing Pay and a courier being booked?",
|
|
5
|
+
"summary": "The API prices the cart from Redis (falling back to Postgres), charges Stripe, records the order and puts it on a RabbitMQ queue; a separate worker books the courier and marks the order booked. The shopper's response returns before the courier is booked.",
|
|
6
|
+
"groups": [
|
|
7
|
+
{"id": "compose", "label": "docker-compose stack"},
|
|
8
|
+
{"id": "data", "label": "Data", "parent": "compose"}
|
|
9
|
+
],
|
|
10
|
+
"nodes": [
|
|
11
|
+
{"id": "shopper", "label": "Shopper's browser", "kind": "client", "assumed": true, "detail": "Nothing in this repo shows the client; the API's POST /checkout is the entry point."},
|
|
12
|
+
{"id": "api", "label": "Checkout API", "kind": "service", "note": "FastAPI · POST /checkout", "group": "compose",
|
|
13
|
+
"evidence": ["api/checkout.py:29-41#def checkout", "docker-compose.yml:2-8"]},
|
|
14
|
+
{"id": "cache", "label": "Price cache", "kind": "cache", "note": "Redis, 5 min TTL", "group": "data",
|
|
15
|
+
"evidence": ["api/checkout.py:20-24#ex=300", "docker-compose.yml:17-18"]},
|
|
16
|
+
{"id": "db", "label": "Orders database", "kind": "database", "note": "Postgres 16", "group": "data",
|
|
17
|
+
"evidence": ["api/checkout.py:22#from products", "api/checkout.py:36#insert into orders", "docker-compose.yml:15-16"]},
|
|
18
|
+
{"id": "broker", "label": "fulfilment queue", "kind": "queue", "note": "RabbitMQ", "group": "compose",
|
|
19
|
+
"evidence": ["api/checkout.py:39-40#routing_key=\"fulfilment\"", "docker-compose.yml:19-20"]},
|
|
20
|
+
{"id": "worker", "label": "Fulfilment worker", "kind": "worker", "group": "compose",
|
|
21
|
+
"evidence": ["worker/fulfil.py:13-19#def on_message", "docker-compose.yml:9-14"]},
|
|
22
|
+
{"id": "stripe", "label": "Stripe", "kind": "external", "note": "PaymentIntent",
|
|
23
|
+
"evidence": ["api/checkout.py:32-35#stripe.PaymentIntent.create"]},
|
|
24
|
+
{"id": "courier", "label": "Courier API", "kind": "external", "note": "courier.example",
|
|
25
|
+
"evidence": ["worker/fulfil.py:16#courier.example/v2/bookings"]}
|
|
26
|
+
],
|
|
27
|
+
"edges": [
|
|
28
|
+
{"id": "pay", "from": "shopper", "to": "api", "label": "POST /checkout", "emphasis": true, "assumed": true},
|
|
29
|
+
{"id": "price", "from": "api", "to": "cache", "label": "get price:<sku>", "evidence": ["api/checkout.py:20"]},
|
|
30
|
+
{"id": "miss", "from": "api", "to": "db", "label": "price on cache miss, insert order", "evidence": ["api/checkout.py:22", "api/checkout.py:36-38"]},
|
|
31
|
+
{"id": "charge", "from": "api", "to": "stripe", "label": "create + confirm payment", "emphasis": true, "evidence": ["api/checkout.py:32-35"]},
|
|
32
|
+
{"id": "publish", "from": "api", "to": "broker", "label": "publish order_id", "style": "dashed", "evidence": ["api/checkout.py:39-40"]},
|
|
33
|
+
{"id": "consume", "from": "broker", "to": "worker", "label": "consume", "style": "dashed", "evidence": ["worker/fulfil.py:22#basic_consume"]},
|
|
34
|
+
{"id": "book", "from": "worker", "to": "courier", "label": "book courier", "evidence": ["worker/fulfil.py:16"]},
|
|
35
|
+
{"id": "mark", "from": "worker", "to": "db", "label": "read address, mark booked", "evidence": ["worker/fulfil.py:15", "worker/fulfil.py:17"]}
|
|
36
|
+
],
|
|
37
|
+
"story": [
|
|
38
|
+
{"text": "The shopper's browser posts the cart to the Checkout API (the client itself is not in this repo).", "edges": ["pay"]},
|
|
39
|
+
{"text": "Each line is priced from Redis; a miss reads Postgres and caches the price for five minutes.", "edges": ["price", "miss"]},
|
|
40
|
+
{"text": "Stripe is charged and confirmed in the same call; anything but 'succeeded' returns 402.", "edges": ["charge"]},
|
|
41
|
+
{"text": "The order is inserted, then its id is published to the fulfilment queue, and the API answers the shopper.", "edges": ["miss", "publish"]},
|
|
42
|
+
{"text": "Later, the worker takes the id, reads the address, books the courier and marks the order booked.", "edges": ["consume", "book", "mark"]}
|
|
43
|
+
],
|
|
44
|
+
"notes": [
|
|
45
|
+
"The worker reads orders.address, but the API's insert does not write an address. Where it comes from is not in this repo.",
|
|
46
|
+
"Left out: the products table's other readers, and anything after the courier booking."
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Worked example — what a checkout request touches
|
|
2
|
+
|
|
3
|
+
## Input
|
|
4
|
+
|
|
5
|
+
> Map what happens in this repo between a shopper pressing Pay and a courier being booked. One
|
|
6
|
+
> HTML file I can send to the team.
|
|
7
|
+
|
|
8
|
+
The repository: [`inputs/checkout/`](inputs/checkout/) — `docker-compose.yml`, `api/checkout.py`
|
|
9
|
+
(FastAPI), `worker/fulfil.py`.
|
|
10
|
+
|
|
11
|
+
## Output
|
|
12
|
+
|
|
13
|
+
[What a checkout request touches](checkout-map.html) — one page, opens offline.
|
|
14
|
+
|
|
15
|
+
The API prices the cart from Redis (falling back to Postgres), charges Stripe, records the order
|
|
16
|
+
and puts its id on a RabbitMQ queue; a separate worker books the courier and marks the order
|
|
17
|
+
booked. The shopper gets their answer before the courier is booked.
|
|
18
|
+
|
|
19
|
+
- **Assumed (dashed):** the shopper's browser and its POST — no client is in this repo.
|
|
20
|
+
- **Worth a look:** the worker reads `orders.address`, but the API's insert never writes one.
|
|
21
|
+
- **Left out:** other readers of the products table, and anything after the courier booking.
|
|
22
|
+
|
|
23
|
+
22 of 22 file citations checked against the repository.
|
|
24
|
+
|
|
25
|
+
## Why this is the right output
|
|
26
|
+
|
|
27
|
+
- Every box and arrow on the page cites the file and lines it came from; click one to see them.
|
|
28
|
+
- The two things the code does not show are drawn dashed, not stated as fact.
|
|
29
|
+
- The map answers the one question asked, and says what it left out.
|
|
30
|
+
- The page was built from [`checkout-map.json`](checkout-map.json) with
|
|
31
|
+
`python3 scripts/build_diagram.py examples/checkout-map.json -o examples/checkout-map.html --repo-root examples/inputs/checkout`.
|
|
@@ -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
|
+
}
|