diff --git a/.gitignore b/.gitignore index 8a3fe1d..a133fef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,9 @@ dist/ *.tsbuildinfo app/m0-app.png app/m1-live.png -app/m2-drill.png app/m3-cost.png -app/m3-flame.png app/m3-flame-real.png +# (m2-drill.png + m3-flame.png are tracked — they illustrate the README's drill-down/flame sections) # capture / render working dirs + large media (regenerable) .rec/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 04d7b90..e7eff29 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -232,9 +232,18 @@ only `?source=live` needs the Node OTLP+SSE server (deferred). base has no routing downside. - **Demo clip.** `app/record.mjs` records the running `app/dist` on the real GPU (serve dist → `channel:'chrome'` + `--force_high_performance_gpu` → wait `[fathom]` ready → `?source=real` → `recordVideo`), then ffmpeg emits - `.mp4` + a two-pass-palette `.gif`. The GIF (`app/fathom-demo.gif`, ~30fps for size) is the README hero and is - git-tracked; `webm`/`mp4` are gitignored (regenerable; the mp4 is uploaded directly at launch). Honesty: the + `.mp4` + a two-pass-palette `.gif` (16fps · 760px · 128 colors · 8s ≈ 6 MB — dense particle motion is + worst-case for gif; the mp4/webm carry full framerate). The GIF (`app/fathom-demo.gif`) is the README hero and + is git-tracked; `webm`/`mp4` are gitignored (regenerable; the mp4 is uploaded directly at launch). Honesty: the clip is a real capture, replayed — the HUD's "requests replayed" / "replayed as a live stream" says so. +- **Launch surface (M5 Fable pass).** `index.html` carries the unfurl cards (`og:`/`twitter:` + + `public/og.png` 1200×630 + `public/favicon.svg`). First-visit failure modes are surfaced, not silent: + `createRiver`/`createFlame` accept `onError` (adapter-null → the shared `NO_ADAPTER_MSG` from + `gpu/capabilities.ts`; init throws → message) rendered in App's full-screen error state; live mode with no + gateway answering shows `ui/LiveNotice.tsx` after 4s (honest "no gateway connected" + how to run one + + one-click switch to replay — it self-dismisses if a local server connects). The river legend hints the M2 + pick (`click a comet → its span`). A minimal mobile pass lives at the end of `styles.css` (≤720px compact + panels; in flame view the flame panel owns the top via `.flame ~ #hud`; ≤480px hides the legend). - **Deferred (live mode).** Hosting the Node server for public `?source=live` needs a live process (Fly.io/Render; Cloudflare Pages is static-only, a Worker+Durable Object is the advanced alt). Not required for the launch demo. diff --git a/CLAUDE.md b/CLAUDE.md index 6dc2226..68c2e6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ v1 build (M0–M4 done; see `SPEC.md`): ## Layout | Path | Role | |---|---| -| `app/` | **cinema** — Vite + React + TS; raw-WebGPU core `app/src/gpu/` (incl. `motion.ts` = pick math mirroring `shaders/river-sim.wgsl` M2/M4 · `bloom.ts` = HDR bloom chain M4), SSE client `app/src/lib/stream.ts`, shell `app/src/ui/` (incl. `SpanDetail.tsx`) | +| `app/` | **cinema** — Vite + React + TS; raw-WebGPU core `app/src/gpu/` (incl. `motion.ts` = pick math mirroring `shaders/river-sim.wgsl` M2/M4 · `bloom.ts` = HDR bloom chain M4), SSE client `app/src/lib/stream.ts`, shell `app/src/ui/` (incl. `SpanDetail.tsx` · `LiveNotice.tsx` M5); `app/public/` = replay data + `og.png`/`favicon.svg` unfurl assets (M5) | | `app/public/traces.json` · `.sample.json` | trace data the app fetches in replay mode (copy of the root files) | | `server/` | **live server** (M1/M2) — OTLP receiver + SSE + `/traces/:id` (`src/otlp.ts`, `src/hub.ts`, `src/index.ts`); `tools/` = emit/live-demo/sentinel-otlp/pick-e2e checks; `e2e.ts` | | `app/tools/pick-check.ts` | M2 pick-math unit harness (mirrors the shader; no GPU) | @@ -133,15 +133,24 @@ GPU/frame with bloom on** — 157× under the 16.7 ms budget on the 4070 (`PROOF `app/m4-richness.png`, also the README hero). Curl samples global time → replay loops drift subtly within ±0.02 NDC (documented caveat); the curl amplitude budget (jitter 0.03 + turb 0.035 + curl 0.015 = 0.080 < 0.09 lane gap) is load-bearing — don't raise it without redoing the arithmetic (pick-check enforces). -**M5 in progress (hosted demo — Opus half done)**: the static replay demo is deployed to **GitHub Pages** -via CI — `.github/workflows/deploy.yml` builds `app/dist` and publishes on every push to `main` -(`ci.yml` gates PRs). The demo runs `?source=real` — the **labeled client-side replay**, no server (live -SSE mode is deferred). Vite `base` is now **`'./'`** (relative) so the bundle works under the Pages subpath -*and* at local root (the recorder serves `dist` at root). The **README hero is now an animated GIF** -(`app/fathom-demo.gif`, tracked) captured by `app/record.mjs` on the 4070 (real capture, replayed — the HUD -says so); `webm`/`mp4` are gitignored (regenerable; mp4 uploaded at launch). GIF is ~30fps for size; the -demo *runs* at 60fps. One-time manual step: repo **Settings → Pages → Source = GitHub Actions**. URL: -`https://manvendra-github.github.io/Fathom/`. Still missing (M5, Fable half): README hero copy + public-demo -visual polish + launch (X/HN). Deferred: hosting the Node OTLP+SSE server for public `?source=live`. -**The v1 build plan is in [`SPEC.md`](./SPEC.md)** (milestones M0–M5). **Next: finish M5 (Fable polish + launch).** +**M5 nearly done (hosted demo — both halves shipped; launch pending)**: the static replay demo is deployed +to **GitHub Pages** via CI — `.github/workflows/deploy.yml` builds `app/dist` and publishes on every push to +`main` (`ci.yml` gates PRs). The demo runs `?source=real` — the **labeled client-side replay**, no server +(live SSE mode is deferred). Vite `base` is **`'./'`** (relative) so the bundle works under the Pages subpath +*and* at local root (the recorder serves `dist` at root). URL: `https://manvendra-github.github.io/Fathom/`. +**Opus half**: deploy/CI + the recorder (`app/record.mjs` → `app/fathom-demo.gif`, tracked README hero; +`webm`/`mp4` gitignored, mp4 uploaded at launch; gif recipe = 16fps·760px·128 colors·8s, shipped in the +recorder — the gif is ~16fps for size, the demo *runs* at 60fps). **Fable half (launch surface)**: README +rebuilt to SPEC §7's shape (hook → hero gif → demo link + the 0.106 ms/157× number → "why this exists" → +substance; `m2-drill.png` + `m3-flame.png` now tracked and embedded); unfurl cards in `app/index.html` +(`og:`/`twitter:` + `app/public/og.png` 1200×630 + `favicon.svg`); **init errors surface on-screen** — +`onError` threaded through `createRiver`/`createFlame` (adapter-null was a silent blank canvas; shared copy +in `gpu/capabilities.ts` `NO_ADAPTER_MSG`); **live mode with no gateway** shows an honest "no gateway +connected" notice after 4s (`ui/LiveNotice.tsx`; self-dismisses on connect, one-click switch to replay); +river legend hints `click a comet → its span`; humane replay-fetch errors; **minimal mobile pass** +(≤720px compact panels + flame-owns-the-top via `.flame ~ #hud`, ≤480px hides the legend). Verified: +pick-check 21/21 · cost-check 50/50 · pick-e2e 8/8 · 4-state screenshot iteration on the 4070 +(`PROOF.md` §8c). Still missing (M5): **launch** (X first, then HN 12–17 UTC — plan in `SPEC.md` §7). +Deferred: hosting the Node OTLP+SSE server for public `?source=live`. +**The v1 build plan is in [`SPEC.md`](./SPEC.md)** (milestones M0–M5). **Next: launch.** Decision context: vault note `next-flagship-project-research.md`. diff --git a/PROOF.md b/PROOF.md index 163e7ee..b47bb4d 100644 --- a/PROOF.md +++ b/PROOF.md @@ -284,7 +284,7 @@ curl flow, model sub-streams, `est. $ saved (cache) —` honest unpriced state). --- -## 8. M5 — hosted demo build + capture (Opus half) +## 8. M5 — hosted demo: build + capture (Opus half) · launch surface (Fable half) The public demo is the static `app/dist` on **GitHub Pages** (CI-built), running `?source=real` — the labeled client-side replay, no server. Deploy config: `.github/workflows/deploy.yml` (build → Pages on `main`) + `ci.yml` (PR build gate). URL: `https://manvendra-github.github.io/Fathom/`. @@ -302,22 +302,34 @@ npm run build **(b) Demo clip captured on the real GPU** (`app/record.mjs` → serve dist → Chrome `--force_high_performance_gpu` → `[fathom]` ready → `?source=real` → `recordVideo` → ffmpeg): ```bash -npm run app:record +npm run app:record # re-run after the M5 Fable polish (hint line, notice, meta) — 2026-07-04 # [record] serving app/dist at http://localhost:8976 source=real -# [page] [fathom] replay mode · 24,982 particles · nvidia · lovelace +# [page] [fathom] replay mode · 25,146 particles · nvidia · lovelace # [record] hero still -> fathom-demo.png # [record] video -> fathom-demo.webm # [record] ffmpeg -> fathom-demo.mp4, fathom-demo.gif -# artifacts: fathom-demo.gif 6.69 MB (760×428, 128 frames, ~30fps) · fathom-demo.mp4 5.32 MB · -# fathom-demo.webm 3.52 MB · fathom-demo.png 0.42 MB +# outputs: fathom-demo.webm 3.43 MB · fathom-demo.mp4 5.15 MB · fathom-demo.gif 6.17 MB +# (gif: 760×428, 16fps × 8s = 128 frames, 128 colors — the recipe now shipped in app/record.mjs) ``` `fathom-demo.gif` is the README hero (git-tracked); `webm`/`mp4` are gitignored (regenerable; the mp4 is uploaded directly at launch). The clip is a **real capture, replayed** — the HUD reads "requests replayed" and the panel says "replayed as a live stream" (honest, no manipulation beyond replay-order interleaving). **M5 Opus exit criterion met:** the static replay demo builds and deploys via CI to a public URL, and the -README autoplay GIF is captured from the shipping bundle on the 4070. GIF is ~30fps for size; the demo -*runs* at 60fps (§7). Remaining (Fable half): README hero copy + public-demo polish + launch. +README autoplay GIF is captured from the shipping bundle on the 4070. GIF is ~16fps for size; the demo +*runs* at 60fps (§7). + +**(c) Fable half — launch surface + first-visit polish** (same-day pass, screenshot-iterated on the 4070): +README rebuilt to the SPEC §7 launch shape (hook → hero gif → demo link + the 0.106 ms/157× number → +"why this exists" → substance); unfurl cards (`og:`/`twitter:` + `og.png` 1200×630 + SVG favicon); +adapter-null and init failures now surface on-screen (`onError` threaded through `createRiver`/`createFlame` +— previously a silent blank canvas); live mode with no gateway shows an honest **"no gateway connected"** +notice after 4s (self-dismisses on connect; one-click switch to the real capture); river legend gains +`click a comet → its span`; replay-fetch errors are worded for humans; minimal mobile pass (≤720px compact +panels, ≤480px canvas-first; flame panel owns the top on phones). Verified: build green · pick-check 21/21 · +cost-check 50/50 · pick-e2e 8/8 (`m2-drill.png` regenerated with the polished UI) · 4-state screenshot +iteration (desktop river / live notice / mobile river / mobile flame — 2 defects caught + fixed: mid-word +code wrapping, flame-over-HUD overlap). Remaining: launch (X/HN). --- diff --git a/README.md b/README.md index 2127884..27d9ce1 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,44 @@ # Fathom -**A WebGPU "observability cinema" for LLM-ops telemetry.** +**I made LLM-gateway telemetry run at 60fps on the GPU.** -Every particle is one real LLM-gateway span. Requests flow as glowing comets into lanes by their -real outcome: **cache hits** stream off as a cyan tributary, **429s** retry and fail over in amber, -**PII** is caught and blocked in-path as red flares, and plain **spans/misses** run as the blue river. +[](https://manvendra-github.github.io/Fathom/) -[](https://manvendra-github.github.io/Fathom/) +> **▶ Live demo — https://manvendra-github.github.io/Fathom/** +> 460 real gateway spans, replayed client-side on *your* GPU (needs a [WebGPU](https://caniuse.com/webgpu) +> browser). The clip above is a real capture, replayed — the HUD says so. The gif is ~16fps for file +> size; the demo itself runs at 60fps. +> +> **The number:** the full pipeline — curl-noise compute, model-shaded sub-streams, HDR bloom — costs +> **0.106 ms of GPU time per frame** on an RTX 4070, measured per-pass with `timestamp-query`. That's +> **157× under the 60fps budget** ([PROOF.md §7](./PROOF.md)). Even *integrated* Intel UHD renders a +> million particles in 3.47 ms. -> **▶ Live demo — https://manvendra-github.github.io/Fathom/** · real sentinel spans, replayed -> client-side at 60fps on the GPU (needs a [WebGPU](https://caniuse.com/webgpu) browser). The clip -> above is a real capture, replayed — the HUD says so. +## Why this exists -> Perf and the real-data look are proven (see [`PROOF.md`](./PROOF.md)); the v1 build is underway per -> [`SPEC.md`](./SPEC.md). **Done: M0** (the app) **+ M1** (a generic OTLP live server — real sentinel spans -> stream in as comets) **+ M2** (drill-down — click a comet → its real span attributes via `GET /traces/:id`) -> **+ M3** (a `river`↔`$ flame` toggle → a rotating **3D cost flame graph**: provider monoliths + model bars -> extruded from the live aggregation, rising embers, orbit camera, hover pick — numbers reconcile with the HUD) -> **+ M4** (richness pass — curl-noise flow in a stateless compute pass, a real HDR **bloom** chain with a -> toggle, model-shaded **sub-streams** within each lane, an honest `est. $ saved (cache)` HUD counter — the -> whole pipeline measured at **0.106 ms GPU/frame** on the 4070, 157× under the 60fps budget). -> **M5 (underway):** the static replay demo is deployed to **GitHub Pages** via CI (link above); -> README hero GIF + launch polish next. +LLM-ops dashboards are flat. Langfuse, Phoenix, Helicone, LiteLLM — tables and 2D charts of the most +consequential telemetry a team has: money leaving per request, PII trying to leave, 429s quietly +eating the latency budget. **Money and PII deserve to be seen.** + +Fathom plugs onto **any** OpenTelemetry-emitting gateway and renders every span as a glowing comet, +laned by its real outcome: **cache hits** stream off as a cyan tributary, **429s** retry and fail +over in amber, **PII** is caught and blocked in-path as red flares, and plain **misses** run as the +blue river. Shade within a lane is the model that served it. + +And it's a tool, not a screensaver: **click any comet** and it opens as a real span — route, outcome, +usage, raw `gen_ai.*` attributes — reconciled against the server's `GET /traces/:id`. + + + +Toggle to the **`$ flame` view** and cost stacks up as a rotating 3D flame graph — provider monoliths, +model bars, rising embers — aggregated from the same spans and reconciled with the HUD *by +construction* (one reduction feeds both). + + + +> **Status:** v1 milestones M0–M4 shipped, M5 (launch) underway — the plan is +> [`SPEC.md`](./SPEC.md), every measured claim is [`PROOF.md`](./PROOF.md), the design is +> [`ARCHITECTURE.md`](./ARCHITECTURE.md). --- @@ -113,6 +130,8 @@ develop the renderer. mixed stream — the load harness emits them in scenario phases, which isn't real arrival timing. Spans and proportions are unchanged; only replay position is shuffled. The panel says "replayed as a live stream." - Model names show as `std`/`pii` — the literal routed-model ids in sentinel's load config, not prettied. +- The hero gif is **~16fps** (gif palettes and file size, not the renderer); measured frame times live + in [`PROOF.md`](./PROOF.md) — 0.106 ms GPU/frame with everything on. - ffmpeg isn't required; without it you get `.webm` (plays everywhere). With it, `record.mjs` also emits `.mp4` + `.gif`. ## Requirements diff --git a/SPEC.md b/SPEC.md index 2beb2cf..383eddb 100644 --- a/SPEC.md +++ b/SPEC.md @@ -124,7 +124,7 @@ Restructuring is **M0 work**, not done in this spec turn. | **M2 ✅ done** | Drill-down | ✅ clicking a comet opens the correct span's real attributes, reconciled against `GET /traces/:id` on the real GPU (see `PROOF.md` §5); pick is CPU math mirroring the shader (`app/src/gpu/motion.ts`) | **Opus** (pick math, `/traces/:id`, wiring) ✅; **Fable** (attribute-panel UI — outcome-lit callout card: reticle + leader line, card parked opposite the comet) ✅ | | **M3 ✅ done** | 3D cost flame graph | ✅ toggle + cost-by-model/provider from the live buffer, reconciles with the HUD on screen; rendered as a **3D WGSL scene** (orbiting monoliths + embers, 4.5 ms/frame — `PROOF.md` §6) | **Opus** (cost aggregation + toggle + reconciliation) ✅; **Fable** (3D WGSL flame: monoliths, embers, orbit camera, ray pick, tracked DOM labels) ✅ | | **M4 ✅ done** | Richness pass | ✅ bloom (HDR post chain + toggle) + curl-noise (stateless compute pass, pick mirror intact) + model sub-streams + `est. $ saved` counter — measured at **0.106 ms GPU/frame** with bloom on (157× headroom, `timestamp-query`; see `PROOF.md` §7) | **Fable** (curl field, bloom look, sub-streams) ✅; **Opus** (perf measurement, $-saved honesty, pick-mirror integrity) ✅ | -| **M5 🚧 in progress** | Hosted demo + launch | **Opus half ✅:** static replay demo deployed to **GitHub Pages** via CI (`deploy.yml` builds `app/dist` on `main`, `ci.yml` gates PRs; `?source=real`, `base:'./'`), README autoplay GIF captured on the 4070 (`app/fathom-demo.gif`, real-capture-replayed, `PROOF.md` §8). **Fable half:** README hero copy + public-demo polish; **launch** (X/HN) pending | **Opus** (deploy/CI config + GIF capture) ✅; **Fable** (README hero + public-demo polish) — pending | +| **M5 🚧 in progress** | Hosted demo + launch | **Opus half ✅:** static replay demo deployed to **GitHub Pages** via CI (`deploy.yml` builds `app/dist` on `main`, `ci.yml` gates PRs; `?source=real`, `base:'./'`), README autoplay GIF captured on the 4070 (`app/fathom-demo.gif`, real-capture-replayed, `PROOF.md` §8). **Fable half ✅:** README rebuilt to the §7 launch shape (hook → hero → number → why); unfurl cards + favicon; init errors surfaced; live-mode "no gateway" notice; river pick hint; mobile pass (`PROOF.md` §8c). **Launch (X/HN) pending** | **Opus** (deploy/CI config + GIF capture) ✅; **Fable** (README hero + public-demo polish) ✅ | **Model policy** (Opus + Fable only): **Fable** for anything people *look at* (shaders, viz, UI polish, launch surface); **Opus** for anything that must be *provably correct* (pick math, cost reconciliation, OTLP mapping) and all plumbing/tests/doc-sync. Highest-value Fable milestone: **M4**. Don't drop below Opus on **M3's aggregation** — a wrong cost number undercuts the "money deserves to be seen" pitch. diff --git a/app/fathom-demo.gif b/app/fathom-demo.gif index 9ee86ac..0a3ff78 100644 Binary files a/app/fathom-demo.gif and b/app/fathom-demo.gif differ diff --git a/app/fathom-demo.png b/app/fathom-demo.png index 7cd5386..35dd25d 100644 Binary files a/app/fathom-demo.png and b/app/fathom-demo.png differ diff --git a/app/index.html b/app/index.html index 107380c..6738499 100644 --- a/app/index.html +++ b/app/index.html @@ -4,6 +4,23 @@
+ Fathom is listening for a live OTLP stream from {server} and hasn't heard
+ back{hostedToLocal ? ' — this hosted demo can only reach a gateway running on your machine' : ''}.
+
+ To feed it real spans: npm run server, then point any OTel gateway at it —{' '}
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4319
+ (own deploy? set VITE_FATHOM_SERVER)
+