]+>.*formatRate\(sustained.accepted_per_second\)/);
+ assert.match(source, /data-plot aria-hidden="true"/);
+ assert.match(source, /height: 200px/);
+ assert.match(source, /font-variant-numeric: tabular-nums/);
+ assert.match(source, /data-rate hidden style=\{`min-width: \$\{rateWidth\}ch`\}><\/span>/);
+ assert.match(source, /const rateWidth = `· \$\{formatRate\(maximum\)\} items\/s, 5 s rate`\.length/);
+ assert.match(source, /\.fo-replay-readout > span \{ white-space: nowrap;/);
+ assert.match(source, /:focus-visible/);
+ assert.match(client, /await import\("uplot"\)/);
+ assert.doesNotMatch(source, //);
+ assert.match(source, /development_preview && {
+ assert.equal(sustained.spans_per_second + sustained.log_records_per_second + sustained.metric_points_per_second,
+ sustained.accepted_per_second);
+ assert.match(source, /\{formatRate\(sustained.accepted_per_second\)\}<\/strong>/);
+ for (const [signal, field] of [["spans", "spans_per_second"], ["logs", "log_records_per_second"], ["metrics", "metric_points_per_second"]]) {
+ assert.ok(source.includes(`{formatRate(sustained.${field})} `));
+ }
+});
diff --git a/site/src/assets/landing/overview-dark.png b/site/src/assets/landing/overview-dark.png
new file mode 100644
index 00000000..8a57d6ef
Binary files /dev/null and b/site/src/assets/landing/overview-dark.png differ
diff --git a/site/src/assets/landing/overview-light.png b/site/src/assets/landing/overview-light.png
new file mode 100644
index 00000000..5b03ff80
Binary files /dev/null and b/site/src/assets/landing/overview-light.png differ
diff --git a/site/src/assets/landing/service-map-dark.png b/site/src/assets/landing/service-map-dark.png
new file mode 100644
index 00000000..7d2f2def
Binary files /dev/null and b/site/src/assets/landing/service-map-dark.png differ
diff --git a/site/src/assets/landing/service-map-light.png b/site/src/assets/landing/service-map-light.png
new file mode 100644
index 00000000..8135a936
Binary files /dev/null and b/site/src/assets/landing/service-map-light.png differ
diff --git a/site/src/assets/landing/trace-dark.png b/site/src/assets/landing/trace-dark.png
new file mode 100644
index 00000000..3ca31c39
Binary files /dev/null and b/site/src/assets/landing/trace-dark.png differ
diff --git a/site/src/assets/landing/trace-light.png b/site/src/assets/landing/trace-light.png
new file mode 100644
index 00000000..79d73a28
Binary files /dev/null and b/site/src/assets/landing/trace-light.png differ
diff --git a/site/src/components/landing/Benchmark.astro b/site/src/components/landing/Benchmark.astro
deleted file mode 100644
index 6f211320..00000000
--- a/site/src/components/landing/Benchmark.astro
+++ /dev/null
@@ -1,119 +0,0 @@
----
-/* Published v2026.10.7-alpha ingest result. The shared report data drives the
- * sustained composition, peak ranges, and the reusable sweep charts here and
- * on the methodology page. Green marks passed gates; throughput uses violet.
- */
-import BenchmarkSweep from "./BenchmarkSweep.astro";
-import { acceptedTotal, benchmark, composition, formatRate, peak, signalNames, type Signal } from "../../data/benchmark";
-
-const signals: Signal[] = ["mixed", "traces", "logs", "metrics"];
-const gates = ["generators exited successfully", "dropped rows 0", "no restarts", "Parquet verified"];
-const compositionTotal = composition.reduce((sum, signal) => sum + signal.rate, 0);
----
-
-
-
-
-
Published benchmark · {benchmark.release}
-
methodology
-
-
Hetzner ccx33 · 8 dedicated vCPUs · 32 GB RAM · separate ccx33 generators
-
-
-
Sustained mixed ingest
-
{formatRate(benchmark.sustained.accepted_per_second)} items/s
-
{benchmark.sustained.seconds} s · {benchmark.sustained.processes_per_signal} processes per signal
- Fanout: {benchmark.sustained.fanout_cpu.toFixed(2)} cores average · {benchmark.sustained.peak_rss_gib.toFixed(1)} GiB peak RSS
- Generator: {benchmark.sustained.generator_cpu.toFixed(2)} of 8 cores average
-
`${formatRate(signal.rate)} ${signal.label.toLowerCase()} per second`).join(", ")}>
- {composition.map((signal, index) => )}
-
-
- {composition.map((signal, index) => {signal.label} {formatRate(signal.rate)}/s )}
-
-
-
-
-
- {signals.map((signal) => {
- const point = peak(signal);
- return
- {signalNames[signal]} median peak
- {formatRate(point.median)} items/s
- {formatRate(point.min)}–{formatRate(point.max)}
- {point.processes_per_signal} processes per signal
-
;
- })}
-
-
-
-
-
diff --git a/site/src/components/landing/BenchmarkSweep.astro b/site/src/components/landing/BenchmarkSweep.astro
index 0f7cff30..5f0951c5 100644
--- a/site/src/components/landing/BenchmarkSweep.astro
+++ b/site/src/components/landing/BenchmarkSweep.astro
@@ -34,10 +34,10 @@ const { sustained } = benchmark;
{signalNames[signal]} accepted throughput by processes per signal
- Median line and minimum-to-maximum band across {benchmark.repeat} repetitions at 1, 2, 4, 8, 16, 32 and 64 processes per signal.
+ Median line and minimum-to-maximum band across the recorded repetitions at each process count; the suite requests {benchmark.repeat} repetitions.
The horizontal axis is evenly spaced on a log2 scale; the vertical axis runs from zero to 600,000 accepted items per second.
- {points.map((point) => ` At ${point.processes_per_signal} processes: ${formatRate(point.median)} items/s, range ${formatRate(point.min)} to ${formatRate(point.max)}.`).join("")}
- {signal === "mixed" && ` The diamond marks the single sustained phase: ${formatRate(sustained.accepted_per_second)} items/s at ${sustained.processes_per_signal} processes per signal over ${sustained.seconds} seconds. Shading at 32–64 processes marks generator CPU of at least 7 cores.`}
+ {points.map((point) => ` At ${point.processes_per_signal} processes: ${formatRate(point.median)} items/s, range ${formatRate(point.min)} to ${formatRate(point.max)}, ${point.repetitions} repetitions.`).join("")}
+ {signal === "mixed" && ` The diamond marks the ${benchmark.development_preview ? "development preview" : "sustained"} phase: ${formatRate(sustained.accepted_per_second)} items/s at ${sustained.processes_per_signal} processes per signal over ${sustained.seconds} seconds. Shading marks generator CPU of at least 7 cores.`}
{limited.length > 0 && <>
@@ -68,7 +68,7 @@ const { sustained } = benchmark;
median
min–max
- {signal === "mixed" && {formatRate(sustained.accepted_per_second)}/s sustained at {sustained.processes_per_signal} }
+ {signal === "mixed" && {formatRate(sustained.accepted_per_second)}/s {benchmark.development_preview ? "preview" : "sustained"} at {sustained.processes_per_signal} }
);
diff --git a/site/src/components/landing/CaptureRow.astro b/site/src/components/landing/CaptureRow.astro
new file mode 100644
index 00000000..363d84fc
--- /dev/null
+++ b/site/src/components/landing/CaptureRow.astro
@@ -0,0 +1,31 @@
+
+
+
diff --git a/site/src/components/landing/Contrast.astro b/site/src/components/landing/Contrast.astro
deleted file mode 100644
index 3ddfa33b..00000000
--- a/site/src/components/landing/Contrast.astro
+++ /dev/null
@@ -1,160 +0,0 @@
----
-/* The page's central argument, shown rather than asserted.
- *
- * Every other claim on this landing page follows from this one: an
- * observability backend is normally five services, and Fanout is one process.
- * Two panes side by side is the form that makes the difference immediate —
- * the left is a list you have to operate, the right is a list you do not.
- *
- * The left column is deliberately not a straw man. Each entry is a real
- * component of a working self-hosted stack, named the way its own project names
- * it, because an unfair comparison is one a reader corrects instead of reading.
- */
-import Icon from "../Icon.astro";
-
-const assembled = [
- { icon: "plugs-connected", part: "Collector", job: "Receive OTLP, batch, forward" },
- { icon: "database", part: "Storage backend", job: "Persist telemetry, expire it" },
- { icon: "magnifying-glass", part: "Query layer", job: "Answer questions over that store" },
- { icon: "chart-line", part: "Dashboard service", job: "Render it for people" },
- { icon: "bell", part: "Alertmanager", job: "Evaluate rules, deliver notifications" },
-];
-
-const single = [
- { icon: "plugs-connected", part: "OTLP in over gRPC and HTTP" },
- { icon: "hard-drives", part: "Authoritative Parquet on local disk" },
- { icon: "magnifying-glass", part: "Indexed traces and DuckDB analytics" },
- { icon: "chart-line", part: "The browser client, served by the binary" },
- { icon: "bell", part: "The alert engine, on a fixed interval" },
-];
----
-
-
-
-
The usual arrangement
-
- {
- assembled.map((row) => (
-
- {row.part}
- {row.job}
-
- ))
- }
-
-
-
-
-
-
Fanout
-
- {single.map((row) => (
- {row.part}
- ))}
-
-
-
-
-
-
diff --git a/site/src/components/landing/CtaBar.astro b/site/src/components/landing/CtaBar.astro
index d97281b2..4f4d3af1 100644
--- a/site/src/components/landing/CtaBar.astro
+++ b/site/src/components/landing/CtaBar.astro
@@ -62,7 +62,7 @@ import Icon from "../Icon.astro";
-
-
diff --git a/site/src/components/landing/Pipeline.astro b/site/src/components/landing/Pipeline.astro
deleted file mode 100644
index c4f8ff04..00000000
--- a/site/src/components/landing/Pipeline.astro
+++ /dev/null
@@ -1,169 +0,0 @@
----
-/* The shape of the product, which happens to be its name.
- *
- * Three signals fan in over one protocol, one process holds them, and the
- * answers fan back out to whoever asked — a browser, the API, an agent. Drawing
- * it as a convergence and a divergence says more than a left-to-right rail
- * does, because the argument is not that the steps are ordered. It is that the
- * middle is a single box, and that everything on both sides speaks to it
- * through interfaces you already have.
- *
- * Inline SVG so it themes with the page through Starlight's tokens; a raster
- * image would need a second copy of itself for light mode. The carries
- * the same content for anyone who cannot see the drawing, and the caption below
- * carries the argument on its own for anyone on a narrow screen where the
- * labels would collide.
- */
-const inputs = [
- { label: "traces", y: 44 },
- { label: "logs", y: 110 },
- { label: "metrics", y: 176 },
-];
-
-const outputs = [
- { label: "browser", y: 44 },
- { label: "API", y: 110 },
- { label: "agent · MCP", y: 176 },
-];
-
-// Fan-in curves flatten as they approach the box, so the three streams read as
-// merging rather than as three arrows that happen to end near each other.
-const inPath = (y: number) =>
- y === 110 ? "M112 110 H300" : `M112 ${y} C 200 ${y}, 214 110, 300 110`;
-
-const outPath = (y: number) =>
- y === 110 ? "M500 110 H688" : `M500 110 C 586 110, 600 ${y}, 688 ${y}`;
----
-
-
-
-
- Traces, logs and metrics arrive over OTLP into a single Fanout process
- that publishes authoritative Parquet with trace indexes and uses DuckDB
- for analytics, and the answers leave to a browser, the HTTP API, or an
- agent over MCP.
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- inputs.map((row) => (
- <>
-
-
- {row.label}
- >
- ))
- }
-
- {
- outputs.map((row) => (
- <>
-
-
- {row.label}
- >
- ))
- }
-
- {/* The whole point of the drawing: one box in the middle. */}
-
- one process
- Parquet + trace indexes
- DuckDB analytics
-
- OTLP
-
-
-
-
-
-
diff --git a/site/src/components/landing/Preview.astro b/site/src/components/landing/Preview.astro
deleted file mode 100644
index e3c45de4..00000000
--- a/site/src/components/landing/Preview.astro
+++ /dev/null
@@ -1,319 +0,0 @@
----
-/* What the product actually looks like.
- *
- * An observability tool's landing page has to show telemetry. Everything else
- * here argues about architecture, which matters to whoever operates it and not
- * at all to whoever has to look at it every day — this is the half of the pitch
- * that says the thing is pleasant to read.
- *
- * Markup rather than a screenshot, for the same reasons the diagram is inline
- * SVG: it themes with the page, stays sharp at any density, needs no second
- * copy for light mode, and cannot go stale in the way an image of a UI does the
- * moment the UI changes.
- *
- * The numbers are illustrative and internally consistent — checkout is the
- * service the hero transcript investigates, with the same p95 and error rate,
- * so a reader who looks twice finds one story rather than two.
- */
-const services: { name: string; health: "ok" | "warn" | "bad"; p95: string; delta: string | null; errors: string; rps: string }[] = [
- { name: "checkout", health: "bad", p95: "412 ms", delta: "+38%", errors: "2.4%", rps: "1.2k" },
- { name: "cart", health: "warn", p95: "187 ms", delta: null, errors: "0.6%", rps: "2.1k" },
- { name: "catalog", health: "ok", p95: "42 ms", delta: null, errors: "0.0%", rps: "3.8k" },
- { name: "payment", health: "ok", p95: "94 ms", delta: null, errors: "0.1%", rps: "980" },
- { name: "auth", health: "ok", p95: "28 ms", delta: null, errors: "0.0%", rps: "5.4k" },
-];
-
-const state = { ok: "healthy", warn: "degraded", bad: "unhealthy" };
-
-// A throughput line with a visible dip where checkout degrades, so the chart
-// agrees with the table beside it.
-const spark = "M0,30 L12,26 L24,28 L36,20 L48,24 L60,16 L72,19 L84,12 L96,15 L108,9 L120,13 L132,22 L144,31 L156,27 L168,18 L180,13 L192,9 L200,7";
----
-
-
-
-
- demo.fanout.run · Overview
- last 1h
-
-
-
-
- Services
- {/* The scroll container is explicit here because the rehype plugin only
- wraps tables that came from Markdown — a table authored inside a
- component reaches the page unwrapped, and check-tables fails the
- build on it rather than letting it clip on a phone. */}
-
-
-
- Service Health p95 Errors Rps
-
-
- {
- services.map((s) => (
-
- {s.name}
-
- {state[s.health]}
-
-
- {s.p95}
- {s.delta && {s.delta} }
-
- {s.errors}
- {s.rps}
-
- ))
- }
-
-
-
-
-
-
-
- Health
-
-
Score 86 +2 vs 1h
-
Spans/min 2.1M stable
-
Errors 0.4% +0.2
-
Services 47 2 alerting
-
-
- Throughput
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/site/src/components/landing/ProductCapture.astro b/site/src/components/landing/ProductCapture.astro
new file mode 100644
index 00000000..e6f15005
--- /dev/null
+++ b/site/src/components/landing/ProductCapture.astro
@@ -0,0 +1,53 @@
+---
+import { Picture } from "astro:assets";
+import overviewLight from "../../assets/landing/overview-light.png";
+import overviewDark from "../../assets/landing/overview-dark.png";
+import mapLight from "../../assets/landing/service-map-light.png";
+import mapDark from "../../assets/landing/service-map-dark.png";
+import traceLight from "../../assets/landing/trace-light.png";
+import traceDark from "../../assets/landing/trace-dark.png";
+
+interface Props {
+ view: "overview" | "service-map" | "trace";
+ alt: string;
+ eager?: boolean;
+}
+
+const { view, alt, eager = false } = Astro.props;
+const captures = {
+ overview: { light: overviewLight, dark: overviewDark },
+ "service-map": { light: mapLight, dark: mapDark },
+ trace: { light: traceLight, dark: traceDark },
+};
+const capture = captures[view];
+const sizes = "(min-width: 71.5rem) 67.5rem, (min-width: 40rem) calc(100vw - 4rem), calc(100vw - 2rem)";
+---
+
+
+ {(["light", "dark"] as const).map(theme => (
+
width <= capture[theme].width && widths.indexOf(width) === index)} sizes={sizes}
+ loading={eager ? "eager" : "lazy"} fetchpriority={eager ? "high" : "auto"} />
+ ))}
+
+
+
diff --git a/site/src/components/landing/ProofStrip.astro b/site/src/components/landing/ProofStrip.astro
index aa956594..19908a64 100644
--- a/site/src/components/landing/ProofStrip.astro
+++ b/site/src/components/landing/ProofStrip.astro
@@ -1,73 +1,46 @@
---
-/* Operational facts immediately below the hero.
- *
- * Fanout does not yet have adoption numbers worth turning into marketing
- * theatre. The useful proof is what the shipped binary actually does, where it
- * runs, and what it does not require. Authored as a Markdown list so the same
- * evidence survives in the Markdown and llms.txt versions of the page.
- */
+import { benchmark, formatRate } from "../../data/benchmark";
+
+// Keep the Markdown statistic readable in llms.txt and enforce the shared report
+// as its authority: a changed result requires updating the authored text too.
+const content = await Astro.slots.render("default");
+const sustained = `${formatRate(benchmark.sustained.accepted_per_second)} items/s`;
+if (!content.includes(`${sustained} `)) {
+ throw new Error(`Landing stat band must use the published sustained rate: ${sustained}`);
+}
---
-
+
diff --git a/site/src/components/landing/Refusals.astro b/site/src/components/landing/Refusals.astro
deleted file mode 100644
index 2e04a3b4..00000000
--- a/site/src/components/landing/Refusals.astro
+++ /dev/null
@@ -1,67 +0,0 @@
----
-/* What Fanout does not do, on the front page rather than buried.
- *
- * A landing page that only lists capabilities makes a reader work out the
- * limits from silence, and these are the limits that decide whether Fanout fits
- * at all. Someone who needs multi-tenancy should learn it in thirty seconds
- * here, not in an hour from a failed evaluation.
- *
- * The content is a Markdown list passed as children, not data in this file.
- * That is the same rule the section headings follow — copy held in a component
- * renders for a person and vanishes from `.md` and llms.txt, and here it
- * would have meant the page's limits being the one thing an agent could not
- * read. An earlier version of this component held the list itself and the page
- * repeated it in Markdown alongside, which rendered every refusal twice.
- *
- * Each item is `**claim** — detail`, so the styling below leans on to
- * find the claim rather than asking the author for a second structure.
- */
----
-
-
-
-
-
-
diff --git a/site/src/components/landing/Section.astro b/site/src/components/landing/Section.astro
deleted file mode 100644
index e1527554..00000000
--- a/site/src/components/landing/Section.astro
+++ /dev/null
@@ -1,80 +0,0 @@
----
-/* The shared shell every landing section sits in.
- *
- * Only the eyebrow is a prop. The heading and the prose are Markdown children,
- * because `.md` and llms.txt are generated from this page's source with
- * component tags stripped — copy passed as a prop renders for a person and
- * vanishes for an agent. That failure is not visible in the browser, which is
- * why the .md route refuses to serve a body-less page rather than trusting
- * anyone to notice.
- *
- * Slotted Markdown means the heading is not in this component's scope, so the
- * type rules below are :global(), reached through .fo-sec rather than applied
- * to the whole page.
- */
-interface Props {
- eyebrow: string;
-}
-
-const { eyebrow } = Astro.props;
----
-
-
-
-
diff --git a/site/src/components/landing/ThroughputReplay.astro b/site/src/components/landing/ThroughputReplay.astro
new file mode 100644
index 00000000..d5ac154e
--- /dev/null
+++ b/site/src/components/landing/ThroughputReplay.astro
@@ -0,0 +1,166 @@
+---
+import { benchmark, sustainedSeries, formatRate } from "../../data/benchmark";
+
+const { sustained, host } = benchmark;
+const id = "throughput-replay";
+const left = 0, right = 1000, top = 0, bottom = 152;
+const seconds = benchmark.series_seconds;
+const totals = sustainedSeries.map(point => point.spans + point.logs + point.metrics);
+const maximum = Math.max(...totals);
+const magnitude = 10 ** Math.floor(Math.log10(maximum / 3));
+const step = [1, 2, 2.5, 5, 10].map(value => value * magnitude).find(value => value * 3 >= maximum)!;
+const ceiling = step * 3;
+const x = (time: number) => left + time / seconds * (right - left);
+const y = (rate: number) => bottom - rate / ceiling * (bottom - top);
+const layers = ["spans", "logs", "metrics"] as const;
+const paths = layers.map((_, layer) => {
+ const bound = (point: typeof sustainedSeries[number], count: number) =>
+ layers.slice(0, count).reduce((sum, key) => sum + point[key], 0);
+ const upper = sustainedSeries.map(point => `${x(point.at_seconds).toFixed(2)},${y(bound(point, layer + 1)).toFixed(2)}`);
+ const lower = [...sustainedSeries].reverse().map(point => `${x(point.at_seconds).toFixed(2)},${y(bound(point, layer)).toFixed(2)}`);
+ return `M${upper.join(" L")} L${lower.join(" L")} Z`;
+});
+const averageDuration = sustained.seconds >= 60 ? `${Math.round(sustained.seconds / 60)}-minute` : `${sustained.seconds}-second`;
+const clock = (time: number) => `${String(Math.floor(time / 60)).padStart(2, "0")}:${String(Math.floor(time % 60)).padStart(2, "0")}`;
+const rateWidth = `· ${formatRate(maximum)} items/s, 5 s rate`.length;
+const machine = host.description.split(",")[0].replace("Cloud ", "");
+const description = `${benchmark.release}: ${formatRate(sustained.accepted_per_second)} accepted items per second on average over ${sustained.seconds} seconds: ${formatRate(sustained.spans_per_second)} spans, ${formatRate(sustained.log_records_per_second)} log records and ${formatRate(sustained.metric_points_per_second)} metric points per second. Stacked areas show trailing five-second rates from Fanout's accepted-row counters, starting with the first full window. ${benchmark.development_preview ? `Development preview of ${benchmark.series_phase}; the suite is incomplete and all gates did not pass.` : "Measured mixed-sustained trial."}`;
+---
+
+
+ {description}
+
+
+ {benchmark.development_preview ? "Development preview · mixed ingest" : "Sustained mixed ingest"}
+
+ {formatRate(sustained.accepted_per_second)} items/s
+
+ {averageDuration} {sustained.phase === "mixed-sustained" ? "average" : "preview average"}
+
+
+ Spans {formatRate(sustained.spans_per_second)}
+ Logs {formatRate(sustained.log_records_per_second)}
+ Metrics {formatRate(sustained.metric_points_per_second)}
+
+ Pause
+
+
+
+ {formatRate(sustained.accepted_per_second)} items/s · {sustained.seconds} seconds of measured mixed ingest
+ {description}
+ {/* Scale only the plotted data; axes keep CSS-pixel geometry and 12px text at every width. */}
+
+
+
+ {[0, step, step * 2, ceiling].map(tick => )}
+
+ {paths.map((path, i) => )}
+
+
+
+ {[0, step, step * 2, ceiling].map(tick => {tick === 0 ? "0" : tick >= 1e6 ? `${tick / 1e6}m` : `${tick / 1000}k`} )}
+
+ {[0, 1, 2, 3, 4].map(tick => {Math.round(seconds * tick / 4)}s )}
+ elapsed seconds
+
+
+
+
+
+
+ Spans
+ Logs
+ Metrics
+
+
+ Full measured run {clock(seconds)} / {clock(seconds)}
+ Replay of the measured run · {machine} · {host.logical_cpus} vCPU
+ {benchmark.development_preview && {benchmark.series_phase} · Partial run · Completion gate failed
}
+
+
+
diff --git a/site/src/components/landing/ThroughputReplayClient.astro b/site/src/components/landing/ThroughputReplayClient.astro
new file mode 100644
index 00000000..7ccf3609
--- /dev/null
+++ b/site/src/components/landing/ThroughputReplayClient.astro
@@ -0,0 +1,8 @@
+{/* Imported by the landing content only: scripts in Starlight's global Hero
+ override are otherwise hoisted onto every documentation page. */}
+
diff --git a/site/src/components/landing/throughput-replay.ts b/site/src/components/landing/throughput-replay.ts
new file mode 100644
index 00000000..003916f1
--- /dev/null
+++ b/site/src/components/landing/throughput-replay.ts
@@ -0,0 +1,203 @@
+import type uPlot from "uplot";
+
+export type Sample = { at_seconds: number; interval_seconds: number; spans: number; logs: number; metrics: number };
+const signals = ["spans", "logs", "metrics"] as const;
+const WINDOW = 120;
+const format = new Intl.NumberFormat("en-US");
+export const clock = (seconds: number) => {
+ const whole = Math.max(0, Math.floor(seconds));
+ return `${String(Math.floor(whole / 60)).padStart(2, "0")}:${String(whole % 60).padStart(2, "0")}`;
+};
+
+// Keep measured timestamps and rates intact, including unequal scrape intervals.
+// Only the time coordinate is offset on repeat. Retain a point before the left
+// edge so the line clips continuously; never fabricate a zero baseline.
+export function replayFrame(samples: Sample[], duration: number, elapsed: number) {
+ const min = elapsed - WINDOW;
+ const points: (Sample & { time: number })[] = [];
+ for (let loop = Math.max(0, Math.floor(min / duration) - 1); loop <= Math.floor(elapsed / duration); loop++) {
+ for (const sample of samples) {
+ const time = loop * duration + sample.at_seconds;
+ if (time <= elapsed) points.push({ ...sample, time });
+ }
+ }
+ const first = points.findIndex(point => point.time >= min);
+ const visible = points.slice(Math.max(0, (first < 0 ? points.length : first) - 1));
+ const current = visible.at(-1)!;
+ // Hold the last received value until the next measured sample arrives.
+ if (current.time < elapsed) visible.push({ ...current, time: elapsed });
+ const data: uPlot.AlignedData = [visible.map(point => point.time), ...signals.map((_, index) =>
+ visible.map(point => signals.slice(0, index + 1).reduce((sum, key) => sum + point[key], 0)))];
+ return { min, max: elapsed, points: visible, current, data };
+}
+
+export function mountReplay(root: HTMLElement, load: () => Promise) {
+ const node = (selector: string) => root.querySelector(selector)!;
+ const host = node("[data-plot]");
+ const button = node("[data-replay]");
+ const elapsedLabel = node("[data-elapsed]");
+ const rateLabel = node("[data-rate]");
+ const stateLabel = node("[data-state]");
+ const tooltip = node("[data-tooltip]");
+ const tooltipTime = node("[data-tooltip-time]");
+ const hoverValues = signals.map(key => node(`[data-hover=${key}]`));
+ const samples: Sample[] = JSON.parse(root.dataset.series!);
+ const duration = Number(root.dataset.seconds);
+ const ceiling = Number(root.dataset.ceiling);
+ const step = Number(root.dataset.step);
+ const motion = matchMedia("(prefers-reduced-motion: reduce)");
+ const finalElapsed = elapsedLabel.textContent;
+ let elapsed = Math.min(samples[0].at_seconds + WINDOW, samples.at(-1)!.at_seconds);
+ let frame = replayFrame(samples, duration, elapsed);
+ let plot: uPlot | undefined;
+ let pending: Promise | undefined;
+ let timer: ReturnType | undefined;
+ let started = 0;
+ let inView = false;
+ let paused = false;
+ let disposed = false;
+ let colors: string[] = [];
+ let ink = "", grid = "", font = "";
+ const visible = () => !disposed && !motion.matches && inView && !document.hidden;
+ const active = () => visible() && !paused;
+ const palette = () => {
+ const style = getComputedStyle(root);
+ colors = signals.map(key => style.getPropertyValue(`--fo-series-${key}`).trim());
+ ink = style.getPropertyValue("--sl-color-gray-2").trim();
+ grid = style.getPropertyValue("--sl-color-gray-5").trim();
+ font = `12px ${style.getPropertyValue("--fo-font-display").trim()}`;
+ };
+ const readouts = () => {
+ elapsedLabel.textContent = `${clock(elapsed % duration)} / ${clock(duration)}`;
+ rateLabel.textContent = `· ${format.format(frame.current.spans + frame.current.logs + frame.current.metrics)} items/s, 5 s rate`;
+ rateLabel.hidden = false;
+ stateLabel.textContent = elapsed >= duration ? `Replay ${Math.floor(elapsed / duration) + 1}` : "Measured stream";
+ };
+ const hover = (chart: uPlot) => {
+ const point = chart.cursor.idx == null ? undefined : frame.points[chart.cursor.idx];
+ tooltip.hidden = chart.cursor.left! < 0 || !point;
+ if (!tooltip.hidden && point) {
+ tooltipTime.textContent = `${clock(point.time % duration)} · items/s, 5 s rate`;
+ hoverValues.forEach((value, i) => { value.textContent = format.format(point[signals[i]]); });
+ }
+ };
+ const resize = () => {
+ const { width, height } = host.getBoundingClientRect();
+ if (plot && width > 0 && height > 0 && (plot.width !== Math.round(width) || plot.height !== Math.round(height)))
+ plot.setSize({ width: Math.round(width), height: Math.round(height) });
+ };
+ const paint = () => {
+ frame = replayFrame(samples, duration, elapsed);
+ plot?.batch(() => {
+ plot!.setData(frame.data, false);
+ plot!.setScale("x", { min: frame.min, max: frame.max });
+ });
+ readouts();
+ };
+ const stop = () => {
+ if (timer !== undefined) {
+ elapsed += (performance.now() - started) / 1000;
+ clearTimeout(timer);
+ timer = undefined;
+ }
+ };
+ const schedule = () => {
+ if (!active() || !plot || timer !== undefined) return;
+ started = performance.now();
+ // Compensate timer drift; hidden/off-screen/user-paused time never counts.
+ timer = setTimeout(() => {
+ elapsed += (performance.now() - started) / 1000;
+ timer = undefined;
+ if (active()) { paint(); schedule(); }
+ }, 1000);
+ };
+ const create = async () => {
+ const Plot = await load();
+ if (!visible() || plot) return;
+ palette();
+ frame = replayFrame(samples, duration, elapsed);
+ const { width, height } = host.getBoundingClientRect();
+ plot = new Plot({
+ width: Math.round(width), height: Math.round(height),
+ legend: { show: false }, select: { show: false, left: 0, top: 0, width: 0, height: 0 },
+ cursor: { drag: { x: false, y: false }, points: { show: false } },
+ scales: { x: { time: false, auto: false, range: () => [frame.min, frame.max] }, y: { auto: false, range: () => [0, ceiling] } },
+ axes: [
+ { stroke: () => ink, font, size: 36, space: 70, grid: { show: false }, ticks: { show: false },
+ values: (_, ticks) => ticks.map(value => value < 0 ? "" : clock(value)) },
+ { stroke: () => ink, font, size: 52, grid: { stroke: () => grid, width: 0.5 }, ticks: { show: false },
+ splits: () => Array.from({ length: 4 }, (_, i) => i * step),
+ values: (_, ticks) => ticks.map(value => value === 0 ? "0" : value >= 1e6 ? `${value / 1e6}m` : `${value / 1000}k`) },
+ ],
+ series: [{}, ...signals.map((key, i) => ({ label: key, stroke: () => colors[i], width: 1,
+ fill: () => `color-mix(in srgb, ${colors[i]} 12%, transparent)`, points: { show: false } }))],
+ bands: [1, 2].map(i => ({ series: [i + 1, i] as [number, number],
+ fill: () => `color-mix(in srgb, ${colors[i]} 12%, transparent)` })),
+ hooks: {
+ setCursor: [hover],
+ draw: [chart => {
+ // Mark every loop boundary in the window without resetting its history.
+ const ctx = chart.ctx;
+ ctx.save(); ctx.strokeStyle = grid; ctx.setLineDash([3 * Plot.pxRatio, 4 * Plot.pxRatio]);
+ ctx.lineWidth = Plot.pxRatio;
+ for (let loop = Math.max(1, Math.ceil(frame.min / duration)); loop * duration <= elapsed; loop++) {
+ const x = Math.round(chart.valToPos(loop * duration, "x", true));
+ ctx.beginPath(); ctx.moveTo(x, chart.bbox.top); ctx.lineTo(x, chart.bbox.top + chart.bbox.height); ctx.stroke();
+ }
+ ctx.restore();
+ }],
+ },
+ }, frame.data, host);
+ plot.root.setAttribute("aria-hidden", "true");
+ root.dataset.mounted = "true";
+ button.hidden = false;
+ readouts();
+ };
+ const sync = () => {
+ if (!active()) stop();
+ if (!visible()) return;
+ if (plot) {
+ palette(); resize(); plot.redraw(false, true); paint(); schedule();
+ } else if (!pending) {
+ pending = create().catch(() => { /* The complete SVG remains usable if the module cannot load. */ })
+ .finally(() => { pending = undefined; if (plot) schedule(); });
+ }
+ };
+ const toggle = () => {
+ paused = !paused;
+ button.textContent = paused ? "Play" : "Pause";
+ button.setAttribute("aria-label", paused ? "Play measured throughput" : "Pause measured throughput");
+ sync();
+ };
+ const onMotion = () => {
+ if (motion.matches) {
+ stop(); plot?.destroy(); plot = undefined;
+ delete root.dataset.mounted;
+ tooltip.hidden = true; button.hidden = true;
+ rateLabel.hidden = true; rateLabel.textContent = "";
+ elapsedLabel.textContent = finalElapsed; stateLabel.textContent = "Full measured run";
+ } else sync();
+ };
+ const intersection = new IntersectionObserver(entries => { inView = entries.some(entry => entry.isIntersecting); sync(); });
+ const size = new ResizeObserver(() => { if (visible()) resize(); });
+ const theme = new MutationObserver(() => { if (visible()) { palette(); plot?.redraw(false, true); } });
+ intersection.observe(root); size.observe(host);
+ theme.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
+ button.addEventListener("click", toggle);
+ motion.addEventListener("change", onMotion);
+ document.addEventListener("visibilitychange", sync);
+ // A bfcache page pauses and resumes; a departing page releases all resources.
+ const onHide = (event: PageTransitionEvent) => { stop(); if (!event.persisted) cleanup(); };
+ const cleanup = () => {
+ if (disposed) return;
+ disposed = true; stop(); plot?.destroy();
+ intersection.disconnect(); size.disconnect(); theme.disconnect();
+ button.removeEventListener("click", toggle); motion.removeEventListener("change", onMotion);
+ document.removeEventListener("visibilitychange", sync);
+ document.removeEventListener("astro:before-swap", cleanup);
+ window.removeEventListener("pagehide", onHide); window.removeEventListener("pageshow", sync);
+ };
+ window.addEventListener("pagehide", onHide); window.addEventListener("pageshow", sync);
+ document.addEventListener("astro:before-swap", cleanup);
+ return cleanup;
+}
diff --git a/site/src/content/docs/explanation/performance.mdx b/site/src/content/docs/explanation/performance.mdx
index 5ffc0f71..f4138307 100644
--- a/site/src/content/docs/explanation/performance.mdx
+++ b/site/src/content/docs/explanation/performance.mdx
@@ -1,7 +1,7 @@
---
title: Performance
-description: Published v2026.10.7-alpha ingest results, hardware, methodology and raw evidence.
-summary: Sustained mixed ingest at 422,408 items/s on a Hetzner ccx33, with three sweep repetitions, ranges, generator CPU and reproduction commands.
+description: Measured ingest rates, hardware, methodology and benchmark evidence.
+summary: Sustained mixed ingest at 415,859 items/s on a Hetzner ccx33.
read_when:
- You want to understand the performance figure on the landing page.
- You need to measure an instance for your own load.
@@ -11,115 +11,118 @@ status: shipped
import BenchmarkSweep from "../../../components/landing/BenchmarkSweep.astro";
On a Hetzner Cloud ccx33 (8 dedicated vCPUs, 32 GB RAM), with telemetrygen
-on a separate ccx33 (8 dedicated vCPUs, 32 GB RAM) in the same location over
-a private network, Fanout v2026.10.7-alpha accepted **422,408 mixed
-OpenTelemetry items/s for five minutes**. The composition was 51,358 spans,
-184,883 log records and 186,178 metric points per second. Every generator
-exited successfully, there were zero dropped rows and no restarts, and
-`fanout repair verify` validated the Parquet store. The run measured ingest only.
+on a separate ccx33 over a private network, Fanout v2026.10.8-alpha accepted
+**415,859 mixed OpenTelemetry items/s** over **303.5 seconds**.
+The composition was 57,037 spans, 179,134 log records and
+179,688 metric points per second. The run measured ingest only.
## Setup and measurement
-Fanout ran alone on its ccx33 with dedicated AMD EPYC-Milan vCPUs. Both VMs
-ran Ubuntu 24.04 in Hetzner's fsn1 location. telemetrygen v0.157.0 sent
-plaintext OTLP/gRPC over the private network, using 1,000-item batches and
-8 workers per process.
+Both hosts were described as **Hetzner Cloud ccx33, fsn1, ubuntu-24.04**. The Fanout host
+reported AMD EPYC-Milan Processor, 8 logical CPUs and
+32,857,198,592 bytes of memory. telemetrygen v0.157.0 sent
+plaintext OTLP/gRPC over the private network, using 1,000-item batches
+and 8 workers per process.
-The `publish` preset swept 1, 2, 4, 8, 16, 32 and 64 processes per signal
-for traces, logs, metrics and mixed ingest, with three repetitions at each
-process count. It then ran one five-minute mixed phase at 32 processes per
-signal. Accepted throughput is the change in Fanout's
-`fanout_ingest_rows_total` counter over each measured phase.
+The `publish` preset ran 1, 2, 4, 8, 16, 32 and 64 processes per signal
+for traces, logs, metrics and mixed ingest, with 3 repetitions per point.
+Accepted throughput is the change
+in Fanout's `fanout_ingest_rows_total` counter over the measured scrape window.
-The suite accepted **584.1 million items** in total: 131,484,819 spans,
-217,735,381 log records and 234,835,485 metric points.
+The suite accepted **575.8 million items** in total: 135,133,541 spans,
+210,698,970 log records and 229,999,166 metric points.
-## Sustained mixed ingest
+## Measured mixed ingest
-The sustained phase ran once for **302.9 seconds**. Fanout averaged
-**7.33 CPU cores** and reached **13.5 GiB peak RSS**. The generator host
-averaged **7.01 of 8 CPU cores**.
+The sustained phase ran for **303.5 seconds** at 32 processes per signal.
+Fanout averaged **7.24 CPU cores** and reached **11.4 GiB peak RSS**.
+The generator host averaged **7.44 of 8 CPU cores**.
| Signal | Accepted items/s |
|---|---:|
-| Spans | 51,358 |
-| Log records | 184,883 |
-| Metric points | 186,178 |
-| Mixed total | 422,408 |
+| Spans | 57,037 |
+| Log records | 179,134 |
+| Metric points | 179,688 |
+| Mixed total | 415,859 |
telemetrygen keeps one trace export in flight per process, while log and
-metric processes keep eight, producing the measured composition.
-
-The run measured ingest only, using synthetic, highly compressible
-telemetrygen payloads. Results describe this hardware.
+metric processes keep eight. Synthetic, highly compressible telemetrygen
+payloads measure ingest capacity on this hardware.
## Median peaks
-Each peak is the highest median accepted rate in that workload's sweep.
-Ranges cover all three repetitions at the listed process count. CPU columns
+Each peak is the highest median accepted rate in that workload's recorded sweep.
+Ranges cover the three repetitions at the listed process count. CPU columns
are median average cores at that count; each host has 8 cores.
-| Workload | Median peak items/s | Processes per signal | Range items/s | Fanout CPU cores | Generator CPU cores |
-|---|---:|---:|---:|---:|---:|
-| Mixed | 429,749 | 32 | 429,504–471,506 | 6.95 | 7.02 |
-| Traces | 243,696 | 64 | 225,110–267,916 | 6.03 | 6.83 |
-| Logs | 342,494 | 64 | 299,902–347,721 | 6.82 | 3.46 |
-| Metrics | 540,102 | 64 | 293,700–543,941 | 5.83 | 4.64 |
+| Workload | Median peak items/s | Processes per signal | Range items/s | Fanout CPU cores | Generator CPU cores | Repetitions |
+|---|---:|---:|---:|---:|---:|---:|
+| Mixed | 402,062 | 32 | 357,956–433,339 | 7.07 | 7.34 | 3 |
+| Traces | 248,901 | 32 | 207,920–295,566 | 6.22 | 5.19 | 3 |
+| Logs | 387,579 | 64 | 353,862–393,976 | 6.85 | 4.63 | 3 |
+| Metrics | 524,878 | 64 | 472,496–561,000 | 6.06 | 5.31 | 3 |
## Mixed sweep
-All rates below are accepted items/s across the three repetitions. CPU
-columns are median average cores per host.
+All rates below are accepted items/s across three repetitions per mixed process
+count. CPU columns are median average cores per host.
| Processes per signal | Median items/s | Min items/s | Max items/s | Fanout CPU cores | Generator CPU cores |
|---|---:|---:|---:|---:|---:|
-| 1 | 41,300 | 40,269 | 41,362 | 4.92 | 0.31 |
-| 2 | 81,075 | 80,661 | 81,651 | 4.44 | 0.65 |
-| 4 | 148,972 | 130,086 | 154,626 | 5.09 | 1.33 |
-| 8 | 244,888 | 219,064 | 273,901 | 6.06 | 2.56 |
-| 16 | 330,232 | 325,618 | 364,301 | 6.39 | 4.29 |
-| 32 | 429,749 | 429,504 | 471,506 | 6.95 | 7.02 |
-| 64 | 422,667 | 384,338 | 441,460 | 6.79 | 7.49 |
+| 1 | 39,041 | 38,296 | 39,149 | 4.72 | 0.80 |
+| 2 | 77,784 | 75,221 | 78,573 | 4.30 | 1.77 |
+| 4 | 146,792 | 146,445 | 155,962 | 4.86 | 1.87 |
+| 8 | 223,985 | 216,861 | 238,993 | 6.06 | 3.03 |
+| 16 | 359,086 | 335,283 | 379,219 | 6.35 | 5.78 |
+| 32 | 402,062 | 357,956 | 433,339 | 7.07 | 7.34 |
+| 64 | 385,274 | 384,289 | 414,315 | 6.86 | 7.35 |
## Signal sweeps
-The charts use a shared vertical scale and an evenly spaced log2 process
-axis. Lines and points show the median; bands show the minimum and maximum
-across three repetitions. The diamond marks the sustained phase at 32
-processes per signal. The shaded region marks generator CPU of at least
-7 cores.
+The charts share a vertical scale and an evenly spaced log2 process axis.
+Lines and points show the median; bands show the recorded minimum and maximum.
+The diamond marks the sustained phase at 32 processes per signal.
+The shaded region marks generator CPU of at least 7 cores.
## Evidence
-The [published evidence directory](https://github.com/labstack/fanout-bench/tree/main/published/2026-10-16-ccx33x2-v2026.10.7-alpha)
-contains the raw artifacts, and its
-[README](https://github.com/labstack/fanout-bench/blob/main/published/2026-10-16-ccx33x2-v2026.10.7-alpha/README.md)
-summarises the run. It records all gates passing: successful generator exits,
-zero dropped rows, no restarts and a verified Parquet store.
+The [published evidence](https://github.com/labstack/fanout-bench/tree/main/published/2026-10-16-ccx33x2-v2026.10.8-alpha)
+contains `report.json` and `samples.jsonl`, produced by
+[Fanout Bench](https://github.com/labstack/fanout-bench) on 16 October 2026 at
+17:36 UTC. All quality gates passed: successful generator exits, zero dropped
+rows, no restarts, authoritative accepted-row counters in every phase and
+verified Parquet storage.
-The harness was clean at commit
-[`8a2a28aafa828fc7967ba82a481c7cdb5c7e921e`](https://github.com/labstack/fanout-bench/commit/8a2a28aafa828fc7967ba82a481c7cdb5c7e921e).
-The [Fanout release artifact](https://github.com/labstack/fanout/releases/download/v2026.10.7-alpha/fanout_v2026.10.7-alpha_linux_amd64.tar.gz)
-has SHA-256:
+The harness commit was
+[`8f5864ba488d9802f68250d85f67b6658ab13af5`](https://github.com/labstack/fanout-bench/commit/8f5864ba488d9802f68250d85f67b6658ab13af5).
+The [Fanout release artifact](https://github.com/labstack/fanout/releases/download/v2026.10.8-alpha/fanout_v2026.10.8-alpha_linux_amd64.tar.gz) has SHA-256:
```text
-d1b602130463b38a03f7575c52c1c2178213839dbf276294abd57ce662c8756b
+b314920221693683b7d6ff42787a37e82bee1fbdd7ddcc8647d1905f7629ac6b
```
## Reproduce
-From the [fanout-bench](https://github.com/labstack/fanout-bench) repository,
-run the Hetzner provisioner with the same release, preset and repetition count:
+From the [fanout-bench](https://github.com/labstack/fanout-bench) repository:
```sh
scripts/hetzner.sh \
- --fanout-version v2026.10.7-alpha \
+ --fanout-version v2026.10.8-alpha \
--preset publish \
--repeat 3
```
Both VMs default to ccx33. The
[publishing guide](https://github.com/labstack/fanout-bench/blob/main/docs/publishing.md)
-documents credentials, provisioning, the private network and artifact collection.
+documents provisioning, the private network and artifact collection.
+
+Regenerate the website's data from the completed run:
+
+```sh
+node site/scripts/benchmark-data.mjs report.json samples.jsonl EVIDENCE_URL
+```
+
+Update authored headline figures with the report; the site build checks them
+against the data.
diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx
index 39c848c4..f2343c74 100644
--- a/site/src/content/docs/index.mdx
+++ b/site/src/content/docs/index.mdx
@@ -1,7 +1,7 @@
---
title: Fanout
description: One binary that ingests OpenTelemetry, stores it, and answers questions about it.
-summary: The landing page — what Fanout is, what it replaces, and what it refuses to do.
+summary: OpenTelemetry observability in one binary, with a system overview, service map and trace waterfall.
template: splash
# The landing page's content title is also the site title, so Starlight's
# default would be "Fanout · Fanout". A frontmatter title replaces that default
@@ -25,7 +25,7 @@ head:
content: "https://fanout.run/"
hero:
title: See what broke without operating an observability stack.
- tagline: Send traces, logs and metrics to one process over OpenTelemetry. Investigate incidents in the browser or through an agent, and run alerts — without operating the stack behind them.
+ tagline: Send OpenTelemetry traces, logs and metrics to one binary to investigate incidents and run alerts.
actions:
- text: Install Fanout
link: /start/install
@@ -34,109 +34,57 @@ hero:
status: shipped
---
-import Section from "../../components/landing/Section.astro";
import ProofStrip from "../../components/landing/ProofStrip.astro";
-import Benchmark from "../../components/landing/Benchmark.astro";
-import Preview from "../../components/landing/Preview.astro";
-import Pipeline from "../../components/landing/Pipeline.astro";
-import Contrast from "../../components/landing/Contrast.astro";
-import Refusals from "../../components/landing/Refusals.astro";
+import CaptureRow from "../../components/landing/CaptureRow.astro";
+import ProductCapture from "../../components/landing/ProductCapture.astro";
import CtaBar from "../../components/landing/CtaBar.astro";
+import ThroughputReplayClient from "../../components/landing/ThroughputReplayClient.astro";
-{/* Prose below is Markdown inside each component, never a prop: `.md` and llms.txt are generated from this source with component tags stripped, so a prop would render for a person and vanish for an agent. Keep MDX comments on one line — toPlainMarkdown only strips single-line ones, and a wrapped comment leaks into the alternate. */}
+
-
-
-- **Self-hosted** on your hardware
-
-- **OTLP native** traces, logs and metrics
+{/* Keep prose as Markdown children: the Markdown and llms.txt routes strip component tags and retain their content. Keep MDX comments on one line. */}
-- **Open local storage** authoritative Parquet, embedded queries
-
-- **One small VM** reference deployment target
+
-- **Apache-2.0** open source
+- [**415,859 items/s** sustained](/explanation/performance)
+- **One binary**
+- **OTLP** traces, logs and metrics
+- **Parquet** you own
+- **Apache-2.0**
-
-
-## Every service, one screen, no query language.
-
-Health, latency, error rate and throughput for everything reporting — with the
-slow one already at the top. The chat investigator works from the same typed
-queries this view does, so what it tells you and what you are looking at cannot
-disagree.
-
-
-
-
-
-
-
-## Five services, or one process.
-
-This is the whole argument. Every other claim on this page follows from it — the
-short backup procedure, the single configuration, and the absence of an
-integration layer to debug at three in the morning.
-
-
-
-
-
-
-
-## Three signals in, one process, answers back out.
-
-Fanout is where your telemetry lands, not something you put in front of the
-thing it lands in. Traces, logs and metrics arrive over standard OTLP from SDKs
-or a Collector you already run, and everything after that happens inside one
-binary.
-
-
-
-Nothing in the middle crosses a network, and nothing else has to be running for
-it to answer. The data directory it writes is the whole of its state.
-
-
+
-
+## Every service on one screen.
-
+See request rate, errors and latency across your system. Open a dashboard
+or ask a question to investigate the services that need attention.
-## 422,408 OpenTelemetry items a second. One binary.
+
-On a Hetzner Cloud ccx33 (8 dedicated vCPUs, 32 GB RAM), with telemetrygen
-on a separate ccx33 in the same location over a private network, Fanout
-v2026.10.7-alpha accepted **422,408 mixed OpenTelemetry items/s for five
-minutes**: 51,358 spans, 184,883 log records and 186,178 metric points per
-second. Every generator exited successfully, there were zero dropped rows and
-no restarts, and the Parquet store passed verification. The run measured ingest only.
+
-
+
-[Read the methodology](/explanation/performance), inspect the
-[raw evidence](https://github.com/labstack/fanout-bench/tree/main/published/2026-10-16-ccx33x2-v2026.10.7-alpha),
-or [run the harness](https://github.com/labstack/fanout-bench) on your own hardware.
+## See how your services connect.
-
+Follow calls across services and external dependencies, with health, traffic and
+latency on the map. Open a service to inspect its traces.
-
+
-## What Fanout will not do.
+
-These decide whether Fanout fits, so they are here rather than three pages in.
-None of them is a roadmap item phrased as a virtue.
+
-
+## Find where the time went.
-- **Not multi-tenant:** namespaces scope queries, not access. Anyone who can query the instance can query every namespace in it; an instance is one trust domain.
-- **No horizontal scale:** ingest, query and maintenance share a process and a machine. There is no scaling one without the others, because they are the same thing.
-- **No universal capacity number:** the benchmark above describes one release, host and workload. Your payload cardinality, query mix and disk decide the capacity of your instance. [Measure your own hardware](/explanation/performance).
-- **No migration paths:** Fanout is pre-release. Storage-format changes may require a fresh data directory; preserve backups before upgrading.
+Open a trace to see each operation in a waterfall, with duration, service and
+error status alongside correlated logs.
-
+
-
+
diff --git a/site/src/content/docs/status/capabilities.mdx b/site/src/content/docs/status/capabilities.mdx
index 670ebe50..192154d9 100644
--- a/site/src/content/docs/status/capabilities.mdx
+++ b/site/src/content/docs/status/capabilities.mdx
@@ -59,11 +59,11 @@ directory.
| Guides for every operational task | preview | Dashboard authoring and inspection are documented; operational coverage is still incomplete. |
| Namespace scoping on alert rules | planned | The field is stored but evaluation ignores it; scope with `service`. |
-## What "no migration path" means in practice
+## Operating limits
-It means what it says. A storage-format change may require starting from an
-empty data directory. The configuration loader rejects unknown `FANOUT_`
-setting names, except platform-injected service-discovery names, so a typo
-surfaces as a refusal to start rather than as a silently ignored value.
+- **Namespaces are not tenants.** Namespaces scope queries, not access. Anyone who can query an instance can query every namespace in it. Each instance is one trust domain.
+- **No horizontal scale.** Ingest, query and maintenance share one process and one machine.
+- **Capacity depends on your workload.** Payload cardinality, query mix and disk determine capacity. The published benchmark measures one release, host and workload. [Measure your own hardware](/explanation/performance).
+- **No migration paths.** Fanout is pre-release. Storage-format changes may require a fresh data directory. Preserve backups before upgrading.
-Do not run a Fanout instance as the only copy of data you need to keep.
+Keep a separate copy of data you need to retain.
diff --git a/site/src/data/benchmark.ts b/site/src/data/benchmark.ts
index 5d74aa9a..f2ef5052 100644
--- a/site/src/data/benchmark.ts
+++ b/site/src/data/benchmark.ts
@@ -1,289 +1,2421 @@
-// Published v2026.10.7-alpha report values. Keep rates at report precision.
-// Derived from published-v2026.10.7.json; raw evidence is linked below.
+// Generated by site/scripts/benchmark-data.mjs. Do not edit numbers by hand.
+// --phase selects development series explicitly; development_preview must stay visible.
export const benchmark = {
- release: "v2026.10.7-alpha",
- evidence:
- "https://github.com/labstack/fanout-bench/tree/main/published/2026-10-16-ccx33x2-v2026.10.7-alpha",
- artifact:
- "https://github.com/labstack/fanout/releases/download/v2026.10.7-alpha/fanout_v2026.10.7-alpha_linux_amd64.tar.gz#sha256=d1b602130463b38a03f7575c52c1c2178213839dbf276294abd57ce662c8756b",
- harness_commit: "8a2a28aafa828fc7967ba82a481c7cdb5c7e921e",
- telemetrygen: "v0.157.0",
- repeat: 3,
- batch_size: 1000,
- accepted_total: {
- spans: 131484819,
- log_records: 217735381,
- metric_points: 234835485,
+ "release": "v2026.10.8-alpha",
+ "evidence": "https://github.com/labstack/fanout-bench/tree/main/published/2026-10-16-ccx33x2-v2026.10.8-alpha",
+ "artifact": "https://github.com/labstack/fanout/releases/download/v2026.10.8-alpha/fanout_v2026.10.8-alpha_linux_amd64.tar.gz#sha256=b314920221693683b7d6ff42787a37e82bee1fbdd7ddcc8647d1905f7629ac6b",
+ "artifact_sha256": "b314920221693683b7d6ff42787a37e82bee1fbdd7ddcc8647d1905f7629ac6b",
+ "harness_commit": "8f5864ba488d9802f68250d85f67b6658ab13af5",
+ "telemetrygen": "v0.157.0",
+ "host": {
+ "description": "Hetzner Cloud ccx33, fsn1, ubuntu-24.04",
+ "generator_description": "Hetzner Cloud ccx33, fsn1, ubuntu-24.04",
+ "cpu_model": "AMD EPYC-Milan Processor",
+ "logical_cpus": 8,
+ "memory_bytes": 32857198592
},
- sustained: {
- processes_per_signal: 32,
- seconds: 302.9,
- accepted_per_second: 422408,
- spans_per_second: 51358,
- log_records_per_second: 184883,
- metric_points_per_second: 186178,
- fanout_cpu: 7.33,
- peak_rss_gib: 13.5,
- generator_cpu: 7.01,
- classification: "saturated-or-generator-limited",
+ "repeat": 3,
+ "batch_size": 1000,
+ "accepted_total": {
+ "spans": 135133541,
+ "log_records": 210698970,
+ "metric_points": 229999166
},
- sweeps: {
- traces: [
+ "sustained": {
+ "phase": "mixed-sustained",
+ "processes_per_signal": 32,
+ "seconds": 303.5,
+ "accepted_per_second": 415859,
+ "spans_per_second": 57037,
+ "log_records_per_second": 179134,
+ "metric_points_per_second": 179688,
+ "fanout_cpu": 7.24,
+ "peak_rss_gib": 11.4,
+ "generator_cpu": 7.44,
+ "classification": "generator-limited"
+ },
+ "sweeps": {
+ "traces": [
{
- processes_per_signal: 1,
- median: 14591,
- min: 14376,
- max: 14631,
- fanout_cpu_median: 4.91,
- generator_cpu_median: 0.13,
+ "processes_per_signal": 1,
+ "median": 13620,
+ "min": 13619,
+ "max": 13699,
+ "fanout_cpu_median": 4.78,
+ "generator_cpu_median": 0.32,
+ "repetitions": 3
},
{
- processes_per_signal: 2,
- median: 29230,
- min: 28622,
- max: 29265,
- fanout_cpu_median: 4.08,
- generator_cpu_median: 0.28,
+ "processes_per_signal": 2,
+ "median": 27131,
+ "min": 26662,
+ "max": 27318,
+ "fanout_cpu_median": 4.75,
+ "generator_cpu_median": 0.7,
+ "repetitions": 3
},
{
- processes_per_signal: 4,
- median: 58232,
- min: 54232,
- max: 58522,
- fanout_cpu_median: 3.68,
- generator_cpu_median: 0.57,
+ "processes_per_signal": 4,
+ "median": 54472,
+ "min": 52886,
+ "max": 54500,
+ "fanout_cpu_median": 3.58,
+ "generator_cpu_median": 1.56,
+ "repetitions": 3
},
{
- processes_per_signal: 8,
- median: 104036,
- min: 103638,
- max: 110067,
- fanout_cpu_median: 4.61,
- generator_cpu_median: 1.19,
+ "processes_per_signal": 8,
+ "median": 107347,
+ "min": 105220,
+ "max": 109337,
+ "fanout_cpu_median": 4.26,
+ "generator_cpu_median": 2.19,
+ "repetitions": 3
},
{
- processes_per_signal: 16,
- median: 182701,
- min: 150682,
- max: 195737,
- fanout_cpu_median: 5.37,
- generator_cpu_median: 2.43,
+ "processes_per_signal": 16,
+ "median": 187925,
+ "min": 149404,
+ "max": 209724,
+ "fanout_cpu_median": 5.21,
+ "generator_cpu_median": 2.82,
+ "repetitions": 3
},
{
- processes_per_signal: 32,
- median: 193558,
- min: 183113,
- max: 284067,
- fanout_cpu_median: 6.27,
- generator_cpu_median: 4.21,
+ "processes_per_signal": 32,
+ "median": 248901,
+ "min": 207920,
+ "max": 295566,
+ "fanout_cpu_median": 6.22,
+ "generator_cpu_median": 5.19,
+ "repetitions": 3
},
{
- processes_per_signal: 64,
- median: 243696,
- min: 225110,
- max: 267916,
- fanout_cpu_median: 6.03,
- generator_cpu_median: 6.83,
- },
+ "processes_per_signal": 64,
+ "median": 221096,
+ "min": 213246,
+ "max": 231153,
+ "fanout_cpu_median": 5.84,
+ "generator_cpu_median": 7.39,
+ "repetitions": 3
+ }
],
- logs: [
+ "logs": [
{
- processes_per_signal: 1,
- median: 13468,
- min: 13138,
- max: 13471,
- fanout_cpu_median: 2.46,
- generator_cpu_median: 0.09,
+ "processes_per_signal": 1,
+ "median": 12634,
+ "min": 12628,
+ "max": 12829,
+ "fanout_cpu_median": 3.63,
+ "generator_cpu_median": 0.22,
+ "repetitions": 3
},
{
- processes_per_signal: 2,
- median: 26532,
- min: 26436,
- max: 26878,
- fanout_cpu_median: 3.15,
- generator_cpu_median: 0.18,
+ "processes_per_signal": 2,
+ "median": 25759,
+ "min": 24687,
+ "max": 25771,
+ "fanout_cpu_median": 0.52,
+ "generator_cpu_median": 0.45,
+ "repetitions": 3
},
{
- processes_per_signal: 4,
- median: 50600,
- min: 49616,
- max: 52532,
- fanout_cpu_median: 6.9,
- generator_cpu_median: 0.35,
+ "processes_per_signal": 4,
+ "median": 49485,
+ "min": 49365,
+ "max": 49973,
+ "fanout_cpu_median": 2.93,
+ "generator_cpu_median": 0.94,
+ "repetitions": 3
},
{
- processes_per_signal: 8,
- median: 100808,
- min: 86979,
- max: 104131,
- fanout_cpu_median: 2.97,
- generator_cpu_median: 0.74,
+ "processes_per_signal": 8,
+ "median": 101384,
+ "min": 101109,
+ "max": 103201,
+ "fanout_cpu_median": 2.02,
+ "generator_cpu_median": 1.63,
+ "repetitions": 3
},
{
- processes_per_signal: 16,
- median: 180022,
- min: 170624,
- max: 185542,
- fanout_cpu_median: 4.97,
- generator_cpu_median: 1.45,
+ "processes_per_signal": 16,
+ "median": 151645,
+ "min": 144120,
+ "max": 184552,
+ "fanout_cpu_median": 7.38,
+ "generator_cpu_median": 2.02,
+ "repetitions": 3
},
{
- processes_per_signal: 32,
- median: 299290,
- min: 247014,
- max: 319885,
- fanout_cpu_median: 5.7,
- generator_cpu_median: 2.75,
+ "processes_per_signal": 32,
+ "median": 282251,
+ "min": 234547,
+ "max": 300455,
+ "fanout_cpu_median": 4.7,
+ "generator_cpu_median": 3.14,
+ "repetitions": 3
},
{
- processes_per_signal: 64,
- median: 342494,
- min: 299902,
- max: 347721,
- fanout_cpu_median: 6.82,
- generator_cpu_median: 3.46,
- },
+ "processes_per_signal": 64,
+ "median": 387579,
+ "min": 353862,
+ "max": 393976,
+ "fanout_cpu_median": 6.85,
+ "generator_cpu_median": 4.63,
+ "repetitions": 3
+ }
],
- metrics: [
+ "metrics": [
{
- processes_per_signal: 1,
- median: 13192,
- min: 12430,
- max: 13395,
- fanout_cpu_median: 5.14,
- generator_cpu_median: 0.08,
+ "processes_per_signal": 1,
+ "median": 12983,
+ "min": 12979,
+ "max": 13064,
+ "fanout_cpu_median": 2.4,
+ "generator_cpu_median": 0.17,
+ "repetitions": 3
},
{
- processes_per_signal: 2,
- median: 26846,
- min: 26708,
- max: 27036,
- fanout_cpu_median: 4.61,
- generator_cpu_median: 0.16,
+ "processes_per_signal": 2,
+ "median": 25338,
+ "min": 25334,
+ "max": 26069,
+ "fanout_cpu_median": 3.8,
+ "generator_cpu_median": 0.36,
+ "repetitions": 3
},
{
- processes_per_signal: 4,
- median: 52641,
- min: 32335,
- max: 53373,
- fanout_cpu_median: 3.37,
- generator_cpu_median: 0.32,
+ "processes_per_signal": 4,
+ "median": 47784,
+ "min": 47331,
+ "max": 48182,
+ "fanout_cpu_median": 4.82,
+ "generator_cpu_median": 0.72,
+ "repetitions": 3
},
{
- processes_per_signal: 8,
- median: 99628,
- min: 95654,
- max: 106147,
- fanout_cpu_median: 3.84,
- generator_cpu_median: 0.63,
+ "processes_per_signal": 8,
+ "median": 86388,
+ "min": 84021,
+ "max": 105921,
+ "fanout_cpu_median": 4.28,
+ "generator_cpu_median": 1.31,
+ "repetitions": 3
},
{
- processes_per_signal: 16,
- median: 184239,
- min: 179830,
- max: 205136,
- fanout_cpu_median: 4.97,
- generator_cpu_median: 1.26,
+ "processes_per_signal": 16,
+ "median": 190894,
+ "min": 189639,
+ "max": 200063,
+ "fanout_cpu_median": 3.82,
+ "generator_cpu_median": 1.8,
+ "repetitions": 3
},
{
- processes_per_signal: 32,
- median: 338541,
- min: 263804,
- max: 341351,
- fanout_cpu_median: 4.56,
- generator_cpu_median: 2.53,
+ "processes_per_signal": 32,
+ "median": 301283,
+ "min": 257260,
+ "max": 351972,
+ "fanout_cpu_median": 5.36,
+ "generator_cpu_median": 2.86,
+ "repetitions": 3
},
{
- processes_per_signal: 64,
- median: 540102,
- min: 293700,
- max: 543941,
- fanout_cpu_median: 5.83,
- generator_cpu_median: 4.64,
- },
+ "processes_per_signal": 64,
+ "median": 524878,
+ "min": 472496,
+ "max": 561000,
+ "fanout_cpu_median": 6.06,
+ "generator_cpu_median": 5.31,
+ "repetitions": 3
+ }
],
- mixed: [
+ "mixed": [
{
- processes_per_signal: 1,
- median: 41300,
- min: 40269,
- max: 41362,
- fanout_cpu_median: 4.92,
- generator_cpu_median: 0.31,
+ "processes_per_signal": 1,
+ "median": 39041,
+ "min": 38296,
+ "max": 39149,
+ "fanout_cpu_median": 4.72,
+ "generator_cpu_median": 0.8,
+ "repetitions": 3
},
{
- processes_per_signal: 2,
- median: 81075,
- min: 80661,
- max: 81651,
- fanout_cpu_median: 4.44,
- generator_cpu_median: 0.65,
+ "processes_per_signal": 2,
+ "median": 77784,
+ "min": 75221,
+ "max": 78573,
+ "fanout_cpu_median": 4.3,
+ "generator_cpu_median": 1.77,
+ "repetitions": 3
},
{
- processes_per_signal: 4,
- median: 148972,
- min: 130086,
- max: 154626,
- fanout_cpu_median: 5.09,
- generator_cpu_median: 1.33,
+ "processes_per_signal": 4,
+ "median": 146792,
+ "min": 146445,
+ "max": 155962,
+ "fanout_cpu_median": 4.86,
+ "generator_cpu_median": 1.87,
+ "repetitions": 3
},
{
- processes_per_signal: 8,
- median: 244888,
- min: 219064,
- max: 273901,
- fanout_cpu_median: 6.06,
- generator_cpu_median: 2.56,
+ "processes_per_signal": 8,
+ "median": 223985,
+ "min": 216861,
+ "max": 238993,
+ "fanout_cpu_median": 6.06,
+ "generator_cpu_median": 3.03,
+ "repetitions": 3
},
{
- processes_per_signal: 16,
- median: 330232,
- min: 325618,
- max: 364301,
- fanout_cpu_median: 6.39,
- generator_cpu_median: 4.29,
+ "processes_per_signal": 16,
+ "median": 359086,
+ "min": 335283,
+ "max": 379219,
+ "fanout_cpu_median": 6.35,
+ "generator_cpu_median": 5.78,
+ "repetitions": 3
},
{
- processes_per_signal: 32,
- median: 429749,
- min: 429504,
- max: 471506,
- fanout_cpu_median: 6.95,
- generator_cpu_median: 7.02,
+ "processes_per_signal": 32,
+ "median": 402062,
+ "min": 357956,
+ "max": 433339,
+ "fanout_cpu_median": 7.07,
+ "generator_cpu_median": 7.34,
+ "repetitions": 3
},
{
- processes_per_signal: 64,
- median: 422667,
- min: 384338,
- max: 441460,
- fanout_cpu_median: 6.79,
- generator_cpu_median: 7.49,
- },
- ],
+ "processes_per_signal": 64,
+ "median": 385274,
+ "min": 384289,
+ "max": 414315,
+ "fanout_cpu_median": 6.86,
+ "generator_cpu_median": 7.35,
+ "repetitions": 3
+ }
+ ]
},
-} as const;
+ "series_phase": "mixed-sustained",
+ "series_seconds": 303.534,
+ "development_preview": false,
+ "quality_gates": [
+ {
+ "name": "generator-processes",
+ "passed": true
+ },
+ {
+ "name": "server-dropped-rows",
+ "passed": true
+ },
+ {
+ "name": "server-restarts",
+ "passed": true
+ },
+ {
+ "name": "authoritative-counters",
+ "passed": true
+ },
+ {
+ "name": "storage-repair-verify",
+ "passed": true
+ }
+ ],
+ "all_gates_passed": true,
+ "sustainedSeries": [
+ {
+ "at_seconds": 7.028,
+ "interval_seconds": 5.007999999999999,
+ "spans": 80471,
+ "logs": 178514,
+ "metrics": 186701
+ },
+ {
+ "at_seconds": 8.023,
+ "interval_seconds": 5.007999999999999,
+ "spans": 69089,
+ "logs": 171725,
+ "metrics": 180112
+ },
+ {
+ "at_seconds": 9.04,
+ "interval_seconds": 6.024999999999999,
+ "spans": 62739,
+ "logs": 167303,
+ "metrics": 175934
+ },
+ {
+ "at_seconds": 10.147,
+ "interval_seconds": 5.1290000000000004,
+ "spans": 35500,
+ "logs": 164944,
+ "metrics": 164165
+ },
+ {
+ "at_seconds": 11.035,
+ "interval_seconds": 5.001,
+ "spans": 25012,
+ "logs": 157968,
+ "metrics": 157169
+ },
+ {
+ "at_seconds": 12.017,
+ "interval_seconds": 5.983,
+ "spans": 35785,
+ "logs": 155440,
+ "metrics": 155440
+ },
+ {
+ "at_seconds": 13.041,
+ "interval_seconds": 5.018000000000001,
+ "spans": 49043,
+ "logs": 195695,
+ "metrics": 194500
+ },
+ {
+ "at_seconds": 14.023,
+ "interval_seconds": 6,
+ "spans": 62350,
+ "logs": 204833,
+ "metrics": 201333
+ },
+ {
+ "at_seconds": 15.018,
+ "interval_seconds": 5.9780000000000015,
+ "spans": 75125,
+ "logs": 222148,
+ "metrics": 216795
+ },
+ {
+ "at_seconds": 16.02,
+ "interval_seconds": 5.872999999999999,
+ "spans": 76796,
+ "logs": 223565,
+ "metrics": 218457
+ },
+ {
+ "at_seconds": 17.015,
+ "interval_seconds": 5.98,
+ "spans": 93819,
+ "logs": 228763,
+ "metrics": 223579
+ },
+ {
+ "at_seconds": 18.019,
+ "interval_seconds": 6.001999999999999,
+ "spans": 98804,
+ "logs": 244585,
+ "metrics": 236921
+ },
+ {
+ "at_seconds": 19.233,
+ "interval_seconds": 5.210000000000001,
+ "spans": 79467,
+ "logs": 197697,
+ "metrics": 199232
+ },
+ {
+ "at_seconds": 20.042,
+ "interval_seconds": 5.024000000000001,
+ "spans": 61111,
+ "logs": 172970,
+ "metrics": 170382
+ },
+ {
+ "at_seconds": 21.042,
+ "interval_seconds": 5.022000000000002,
+ "spans": 62728,
+ "logs": 172840,
+ "metrics": 177021
+ },
+ {
+ "at_seconds": 22.032,
+ "interval_seconds": 5.0169999999999995,
+ "spans": 43855,
+ "logs": 161850,
+ "metrics": 167032
+ },
+ {
+ "at_seconds": 23.029,
+ "interval_seconds": 5.010000000000002,
+ "spans": 30351,
+ "logs": 152495,
+ "metrics": 158084
+ },
+ {
+ "at_seconds": 24.092,
+ "interval_seconds": 6.073,
+ "spans": 37388,
+ "logs": 164334,
+ "metrics": 167957
+ },
+ {
+ "at_seconds": 25.017,
+ "interval_seconds": 5.783999999999999,
+ "spans": 43578,
+ "logs": 196577,
+ "metrics": 194675
+ },
+ {
+ "at_seconds": 26.02,
+ "interval_seconds": 5.977999999999998,
+ "spans": 55212,
+ "logs": 208264,
+ "metrics": 208933
+ },
+ {
+ "at_seconds": 27.102,
+ "interval_seconds": 5.07,
+ "spans": 73578,
+ "logs": 223471,
+ "metrics": 216963
+ },
+ {
+ "at_seconds": 28.019,
+ "interval_seconds": 5.986999999999998,
+ "spans": 78343,
+ "logs": 225823,
+ "metrics": 222315
+ },
+ {
+ "at_seconds": 29.014,
+ "interval_seconds": 5.984999999999999,
+ "spans": 80368,
+ "logs": 233083,
+ "metrics": 229908
+ },
+ {
+ "at_seconds": 30.016,
+ "interval_seconds": 5.9239999999999995,
+ "spans": 83727,
+ "logs": 224173,
+ "metrics": 226199
+ },
+ {
+ "at_seconds": 31.029,
+ "interval_seconds": 5.009,
+ "spans": 89040,
+ "logs": 226392,
+ "metrics": 230186
+ },
+ {
+ "at_seconds": 32.015,
+ "interval_seconds": 5.995000000000001,
+ "spans": 85071,
+ "logs": 232193,
+ "metrics": 235196
+ },
+ {
+ "at_seconds": 33.225,
+ "interval_seconds": 5.206000000000003,
+ "spans": 73185,
+ "logs": 194199,
+ "metrics": 194775
+ },
+ {
+ "at_seconds": 34.108,
+ "interval_seconds": 5.093999999999998,
+ "spans": 62230,
+ "logs": 179034,
+ "metrics": 185120
+ },
+ {
+ "at_seconds": 35.019,
+ "interval_seconds": 5.003,
+ "spans": 52980,
+ "logs": 196282,
+ "metrics": 197482
+ },
+ {
+ "at_seconds": 36.023,
+ "interval_seconds": 6.007000000000005,
+ "spans": 49785,
+ "logs": 174963,
+ "metrics": 174796
+ },
+ {
+ "at_seconds": 37.027,
+ "interval_seconds": 5.0120000000000005,
+ "spans": 36923,
+ "logs": 154429,
+ "metrics": 153631
+ },
+ {
+ "at_seconds": 38.025,
+ "interval_seconds": 6.009999999999998,
+ "spans": 46268,
+ "logs": 157404,
+ "metrics": 157238
+ },
+ {
+ "at_seconds": 39.03,
+ "interval_seconds": 5.805,
+ "spans": 49280,
+ "logs": 196210,
+ "metrics": 198966
+ },
+ {
+ "at_seconds": 40.019,
+ "interval_seconds": 5,
+ "spans": 49402,
+ "logs": 154000,
+ "metrics": 154800
+ },
+ {
+ "at_seconds": 41.04,
+ "interval_seconds": 5.016999999999996,
+ "spans": 48836,
+ "logs": 154674,
+ "metrics": 152482
+ },
+ {
+ "at_seconds": 42.057,
+ "interval_seconds": 5.030000000000001,
+ "spans": 44750,
+ "logs": 158052,
+ "metrics": 156660
+ },
+ {
+ "at_seconds": 43.019,
+ "interval_seconds": 5.991999999999997,
+ "spans": 42906,
+ "logs": 153705,
+ "metrics": 157543
+ },
+ {
+ "at_seconds": 44.03,
+ "interval_seconds": 5,
+ "spans": 30421,
+ "logs": 139600,
+ "metrics": 142400
+ },
+ {
+ "at_seconds": 45.021,
+ "interval_seconds": 5.0020000000000024,
+ "spans": 50801,
+ "logs": 195322,
+ "metrics": 194322
+ },
+ {
+ "at_seconds": 46.024,
+ "interval_seconds": 6.005000000000003,
+ "spans": 62965,
+ "logs": 200833,
+ "metrics": 201499
+ },
+ {
+ "at_seconds": 47.02,
+ "interval_seconds": 5.980000000000004,
+ "spans": 79616,
+ "logs": 231940,
+ "metrics": 236288
+ },
+ {
+ "at_seconds": 48.018,
+ "interval_seconds": 5.9609999999999985,
+ "spans": 91600,
+ "logs": 232511,
+ "metrics": 236537
+ },
+ {
+ "at_seconds": 49.018,
+ "interval_seconds": 5.999000000000002,
+ "spans": 99854,
+ "logs": 252042,
+ "metrics": 250708
+ },
+ {
+ "at_seconds": 50.036,
+ "interval_seconds": 5.015000000000001,
+ "spans": 93918,
+ "logs": 227517,
+ "metrics": 230110
+ },
+ {
+ "at_seconds": 51.073,
+ "interval_seconds": 5.0489999999999995,
+ "spans": 87740,
+ "logs": 229748,
+ "metrics": 227768
+ },
+ {
+ "at_seconds": 52.019,
+ "interval_seconds": 5.994999999999997,
+ "spans": 90409,
+ "logs": 227356,
+ "metrics": 226689
+ },
+ {
+ "at_seconds": 53.216,
+ "interval_seconds": 5.198,
+ "spans": 66756,
+ "logs": 197384,
+ "metrics": 196806
+ },
+ {
+ "at_seconds": 54.029,
+ "interval_seconds": 5.011000000000003,
+ "spans": 58671,
+ "logs": 172421,
+ "metrics": 172620
+ },
+ {
+ "at_seconds": 55.06,
+ "interval_seconds": 5.024000000000001,
+ "spans": 54148,
+ "logs": 188296,
+ "metrics": 188495
+ },
+ {
+ "at_seconds": 56.206,
+ "interval_seconds": 5.133000000000003,
+ "spans": 39169,
+ "logs": 146503,
+ "metrics": 146308
+ },
+ {
+ "at_seconds": 57.032,
+ "interval_seconds": 5.012999999999998,
+ "spans": 30540,
+ "logs": 160582,
+ "metrics": 159186
+ },
+ {
+ "at_seconds": 58.115,
+ "interval_seconds": 6.096000000000004,
+ "spans": 30364,
+ "logs": 149606,
+ "metrics": 150262
+ },
+ {
+ "at_seconds": 59.329,
+ "interval_seconds": 5.299999999999997,
+ "spans": 31158,
+ "logs": 165283,
+ "metrics": 166226
+ },
+ {
+ "at_seconds": 60.029,
+ "interval_seconds": 6,
+ "spans": 29856,
+ "logs": 159167,
+ "metrics": 159000
+ },
+ {
+ "at_seconds": 61.151,
+ "interval_seconds": 6.091000000000001,
+ "spans": 36468,
+ "logs": 152520,
+ "metrics": 151699
+ },
+ {
+ "at_seconds": 62.026,
+ "interval_seconds": 5.82,
+ "spans": 39366,
+ "logs": 171478,
+ "metrics": 173368
+ },
+ {
+ "at_seconds": 63.156,
+ "interval_seconds": 5.040999999999997,
+ "spans": 32355,
+ "logs": 152351,
+ "metrics": 152351
+ },
+ {
+ "at_seconds": 64.222,
+ "interval_seconds": 6.106999999999992,
+ "spans": 33590,
+ "logs": 167676,
+ "metrics": 167676
+ },
+ {
+ "at_seconds": 65.04,
+ "interval_seconds": 5.011000000000003,
+ "spans": 30552,
+ "logs": 155059,
+ "metrics": 159050
+ },
+ {
+ "at_seconds": 66.025,
+ "interval_seconds": 5.996000000000002,
+ "spans": 34376,
+ "logs": 176618,
+ "metrics": 179453
+ },
+ {
+ "at_seconds": 67.039,
+ "interval_seconds": 5.012999999999998,
+ "spans": 34528,
+ "logs": 171354,
+ "metrics": 174347
+ },
+ {
+ "at_seconds": 68.172,
+ "interval_seconds": 5.015999999999998,
+ "spans": 50051,
+ "logs": 191986,
+ "metrics": 192584
+ },
+ {
+ "at_seconds": 69.016,
+ "interval_seconds": 5.8600000000000065,
+ "spans": 53763,
+ "logs": 209386,
+ "metrics": 209898
+ },
+ {
+ "at_seconds": 70.018,
+ "interval_seconds": 5.7960000000000065,
+ "spans": 65566,
+ "logs": 211698,
+ "metrics": 212215
+ },
+ {
+ "at_seconds": 71.022,
+ "interval_seconds": 5.981999999999999,
+ "spans": 76566,
+ "logs": 218656,
+ "metrics": 213641
+ },
+ {
+ "at_seconds": 72.072,
+ "interval_seconds": 5.033000000000001,
+ "spans": 88814,
+ "logs": 229287,
+ "metrics": 223525
+ },
+ {
+ "at_seconds": 73.024,
+ "interval_seconds": 5.984999999999999,
+ "spans": 90894,
+ "logs": 235589,
+ "metrics": 230744
+ },
+ {
+ "at_seconds": 74.015,
+ "interval_seconds": 5.8430000000000035,
+ "spans": 82321,
+ "logs": 212733,
+ "metrics": 211364
+ },
+ {
+ "at_seconds": 75.014,
+ "interval_seconds": 5.9979999999999905,
+ "spans": 77030,
+ "logs": 214905,
+ "metrics": 210070
+ },
+ {
+ "at_seconds": 76.033,
+ "interval_seconds": 5.010999999999996,
+ "spans": 62268,
+ "logs": 186989,
+ "metrics": 187188
+ },
+ {
+ "at_seconds": 77.015,
+ "interval_seconds": 5.992999999999995,
+ "spans": 63096,
+ "logs": 202236,
+ "metrics": 202903
+ },
+ {
+ "at_seconds": 78.014,
+ "interval_seconds": 5.941999999999993,
+ "spans": 65320,
+ "logs": 203467,
+ "metrics": 202289
+ },
+ {
+ "at_seconds": 79.014,
+ "interval_seconds": 5.989999999999995,
+ "spans": 66633,
+ "logs": 201836,
+ "metrics": 200668
+ },
+ {
+ "at_seconds": 80.018,
+ "interval_seconds": 5.004000000000005,
+ "spans": 84753,
+ "logs": 215627,
+ "metrics": 216427
+ },
+ {
+ "at_seconds": 81.054,
+ "interval_seconds": 5.021000000000001,
+ "spans": 76102,
+ "logs": 205935,
+ "metrics": 204143
+ },
+ {
+ "at_seconds": 82.068,
+ "interval_seconds": 5.052999999999997,
+ "spans": 68870,
+ "logs": 192361,
+ "metrics": 191174
+ },
+ {
+ "at_seconds": 83.035,
+ "interval_seconds": 5.021000000000001,
+ "spans": 51787,
+ "logs": 159729,
+ "metrics": 163712
+ },
+ {
+ "at_seconds": 84.032,
+ "interval_seconds": 5.018000000000001,
+ "spans": 36672,
+ "logs": 135711,
+ "metrics": 139299
+ },
+ {
+ "at_seconds": 85.047,
+ "interval_seconds": 5.028999999999996,
+ "spans": 26458,
+ "logs": 136807,
+ "metrics": 139789
+ },
+ {
+ "at_seconds": 86.015,
+ "interval_seconds": 5.997,
+ "spans": 34860,
+ "logs": 156412,
+ "metrics": 158246
+ },
+ {
+ "at_seconds": 87.048,
+ "interval_seconds": 5.994,
+ "spans": 39883,
+ "logs": 166667,
+ "metrics": 168335
+ },
+ {
+ "at_seconds": 88.157,
+ "interval_seconds": 5.122,
+ "spans": 42959,
+ "logs": 164975,
+ "metrics": 163803
+ },
+ {
+ "at_seconds": 89.375,
+ "interval_seconds": 5.3430000000000035,
+ "spans": 39689,
+ "logs": 164327,
+ "metrics": 164701
+ },
+ {
+ "at_seconds": 90.034,
+ "interval_seconds": 6.0020000000000095,
+ "spans": 38664,
+ "logs": 175775,
+ "metrics": 178441
+ },
+ {
+ "at_seconds": 91.032,
+ "interval_seconds": 5.016999999999996,
+ "spans": 40469,
+ "logs": 161052,
+ "metrics": 166235
+ },
+ {
+ "at_seconds": 92.025,
+ "interval_seconds": 6.010000000000005,
+ "spans": 52584,
+ "logs": 175042,
+ "metrics": 179201
+ },
+ {
+ "at_seconds": 93.019,
+ "interval_seconds": 5.971000000000004,
+ "spans": 64986,
+ "logs": 207335,
+ "metrics": 212360
+ },
+ {
+ "at_seconds": 94.028,
+ "interval_seconds": 5.871000000000009,
+ "spans": 79378,
+ "logs": 229773,
+ "metrics": 233521
+ },
+ {
+ "at_seconds": 95.015,
+ "interval_seconds": 5.640000000000001,
+ "spans": 97874,
+ "logs": 254255,
+ "metrics": 255319
+ },
+ {
+ "at_seconds": 96.023,
+ "interval_seconds": 5.98899999999999,
+ "spans": 94174,
+ "logs": 229588,
+ "metrics": 228920
+ },
+ {
+ "at_seconds": 97.014,
+ "interval_seconds": 5.981999999999999,
+ "spans": 94952,
+ "logs": 232865,
+ "metrics": 231695
+ },
+ {
+ "at_seconds": 98.015,
+ "interval_seconds": 5.989999999999995,
+ "spans": 90150,
+ "logs": 234891,
+ "metrics": 234558
+ },
+ {
+ "at_seconds": 99.022,
+ "interval_seconds": 6.003,
+ "spans": 84458,
+ "logs": 223721,
+ "metrics": 221223
+ },
+ {
+ "at_seconds": 100.239,
+ "interval_seconds": 5.224000000000004,
+ "spans": 62404,
+ "logs": 177259,
+ "metrics": 178599
+ },
+ {
+ "at_seconds": 101.051,
+ "interval_seconds": 5.028000000000006,
+ "spans": 63843,
+ "logs": 177804,
+ "metrics": 178401
+ },
+ {
+ "at_seconds": 102.198,
+ "interval_seconds": 5.1839999999999975,
+ "spans": 47075,
+ "logs": 165509,
+ "metrics": 167052
+ },
+ {
+ "at_seconds": 103.056,
+ "interval_seconds": 5.040999999999997,
+ "spans": 37698,
+ "logs": 148978,
+ "metrics": 148185
+ },
+ {
+ "at_seconds": 104.021,
+ "interval_seconds": 6.006,
+ "spans": 48294,
+ "logs": 168498,
+ "metrics": 167000
+ },
+ {
+ "at_seconds": 105.046,
+ "interval_seconds": 6.024000000000001,
+ "spans": 49976,
+ "logs": 162849,
+ "metrics": 165505
+ },
+ {
+ "at_seconds": 106.024,
+ "interval_seconds": 5.784999999999997,
+ "spans": 50312,
+ "logs": 172342,
+ "metrics": 175627
+ },
+ {
+ "at_seconds": 107.064,
+ "interval_seconds": 6.012999999999991,
+ "spans": 48571,
+ "logs": 181939,
+ "metrics": 185265
+ },
+ {
+ "at_seconds": 108.173,
+ "interval_seconds": 5.117000000000004,
+ "spans": 49059,
+ "logs": 159859,
+ "metrics": 165918
+ },
+ {
+ "at_seconds": 109.014,
+ "interval_seconds": 5.957999999999998,
+ "spans": 47673,
+ "logs": 180262,
+ "metrics": 185465
+ },
+ {
+ "at_seconds": 110.021,
+ "interval_seconds": 6,
+ "spans": 40509,
+ "logs": 146000,
+ "metrics": 154000
+ },
+ {
+ "at_seconds": 111.256,
+ "interval_seconds": 5.231999999999999,
+ "spans": 38046,
+ "logs": 167240,
+ "metrics": 171063
+ },
+ {
+ "at_seconds": 112.042,
+ "interval_seconds": 6.018000000000001,
+ "spans": 33077,
+ "logs": 152542,
+ "metrics": 152376
+ },
+ {
+ "at_seconds": 113.023,
+ "interval_seconds": 5.959000000000003,
+ "spans": 37436,
+ "logs": 163786,
+ "metrics": 164625
+ },
+ {
+ "at_seconds": 114.032,
+ "interval_seconds": 5.018000000000001,
+ "spans": 41265,
+ "logs": 145476,
+ "metrics": 142886
+ },
+ {
+ "at_seconds": 115.033,
+ "interval_seconds": 5.0120000000000005,
+ "spans": 34728,
+ "logs": 185355,
+ "metrics": 180966
+ },
+ {
+ "at_seconds": 116.073,
+ "interval_seconds": 6.0519999999999925,
+ "spans": 40988,
+ "logs": 181097,
+ "metrics": 176305
+ },
+ {
+ "at_seconds": 117.018,
+ "interval_seconds": 5.7620000000000005,
+ "spans": 37497,
+ "logs": 168344,
+ "metrics": 168865
+ },
+ {
+ "at_seconds": 118.092,
+ "interval_seconds": 5.069000000000003,
+ "spans": 41640,
+ "logs": 172223,
+ "metrics": 173210
+ },
+ {
+ "at_seconds": 119.043,
+ "interval_seconds": 5.01100000000001,
+ "spans": 47510,
+ "logs": 194173,
+ "metrics": 195969
+ },
+ {
+ "at_seconds": 120.047,
+ "interval_seconds": 5.013999999999996,
+ "spans": 48473,
+ "logs": 175907,
+ "metrics": 174711
+ },
+ {
+ "at_seconds": 121.078,
+ "interval_seconds": 5.00500000000001,
+ "spans": 39969,
+ "logs": 165634,
+ "metrics": 169830
+ },
+ {
+ "at_seconds": 122.249,
+ "interval_seconds": 5.2309999999999945,
+ "spans": 40162,
+ "logs": 182565,
+ "metrics": 180463
+ },
+ {
+ "at_seconds": 123.021,
+ "interval_seconds": 6.003,
+ "spans": 40661,
+ "logs": 167416,
+ "metrics": 165750
+ },
+ {
+ "at_seconds": 124.017,
+ "interval_seconds": 5.924999999999997,
+ "spans": 43045,
+ "logs": 179072,
+ "metrics": 180591
+ },
+ {
+ "at_seconds": 125.034,
+ "interval_seconds": 5.991,
+ "spans": 43572,
+ "logs": 180270,
+ "metrics": 180938
+ },
+ {
+ "at_seconds": 126.042,
+ "interval_seconds": 5.9950000000000045,
+ "spans": 49048,
+ "logs": 189825,
+ "metrics": 193161
+ },
+ {
+ "at_seconds": 127.144,
+ "interval_seconds": 6.0660000000000025,
+ "spans": 48638,
+ "logs": 173755,
+ "metrics": 172601
+ },
+ {
+ "at_seconds": 128.016,
+ "interval_seconds": 5.766999999999996,
+ "spans": 43871,
+ "logs": 175655,
+ "metrics": 174961
+ },
+ {
+ "at_seconds": 129.495,
+ "interval_seconds": 5.478000000000009,
+ "spans": 40353,
+ "logs": 160643,
+ "metrics": 160095
+ },
+ {
+ "at_seconds": 130.035,
+ "interval_seconds": 5.000999999999991,
+ "spans": 31805,
+ "logs": 144571,
+ "metrics": 150170
+ },
+ {
+ "at_seconds": 131.022,
+ "interval_seconds": 5.987999999999985,
+ "spans": 48442,
+ "logs": 163828,
+ "metrics": 169339
+ },
+ {
+ "at_seconds": 132.016,
+ "interval_seconds": 5.9739999999999895,
+ "spans": 54247,
+ "logs": 166890,
+ "metrics": 169736
+ },
+ {
+ "at_seconds": 133.016,
+ "interval_seconds": 5,
+ "spans": 65613,
+ "logs": 199800,
+ "metrics": 203800
+ },
+ {
+ "at_seconds": 134.021,
+ "interval_seconds": 6.0049999999999955,
+ "spans": 70452,
+ "logs": 193339,
+ "metrics": 195504
+ },
+ {
+ "at_seconds": 135.019,
+ "interval_seconds": 5.524000000000001,
+ "spans": 83095,
+ "logs": 221941,
+ "metrics": 219768
+ },
+ {
+ "at_seconds": 136.06,
+ "interval_seconds": 5.038000000000011,
+ "spans": 79794,
+ "logs": 221715,
+ "metrics": 215959
+ },
+ {
+ "at_seconds": 137.015,
+ "interval_seconds": 5.992999999999995,
+ "spans": 85934,
+ "logs": 229267,
+ "metrics": 224428
+ },
+ {
+ "at_seconds": 138.059,
+ "interval_seconds": 5.043000000000006,
+ "spans": 88241,
+ "logs": 215150,
+ "metrics": 214555
+ },
+ {
+ "at_seconds": 139.066,
+ "interval_seconds": 5.045000000000016,
+ "spans": 75719,
+ "logs": 201784,
+ "metrics": 198811
+ },
+ {
+ "at_seconds": 140.056,
+ "interval_seconds": 5.037000000000006,
+ "spans": 57974,
+ "logs": 183840,
+ "metrics": 190590
+ },
+ {
+ "at_seconds": 141.156,
+ "interval_seconds": 5.096000000000004,
+ "spans": 46723,
+ "logs": 185243,
+ "metrics": 190542
+ },
+ {
+ "at_seconds": 142.048,
+ "interval_seconds": 5.0330000000000155,
+ "spans": 30221,
+ "logs": 153388,
+ "metrics": 153586
+ },
+ {
+ "at_seconds": 143.017,
+ "interval_seconds": 6.0020000000000095,
+ "spans": 34844,
+ "logs": 171276,
+ "metrics": 171443
+ },
+ {
+ "at_seconds": 144.017,
+ "interval_seconds": 5.957999999999998,
+ "spans": 45844,
+ "logs": 185297,
+ "metrics": 180933
+ },
+ {
+ "at_seconds": 145.015,
+ "interval_seconds": 5.948999999999984,
+ "spans": 54654,
+ "logs": 208438,
+ "metrics": 209783
+ },
+ {
+ "at_seconds": 146.045,
+ "interval_seconds": 5.988999999999976,
+ "spans": 66476,
+ "logs": 204041,
+ "metrics": 197863
+ },
+ {
+ "at_seconds": 147.021,
+ "interval_seconds": 5.864999999999981,
+ "spans": 69231,
+ "logs": 192668,
+ "metrics": 191134
+ },
+ {
+ "at_seconds": 148.029,
+ "interval_seconds": 5.0120000000000005,
+ "spans": 73925,
+ "logs": 184557,
+ "metrics": 184956
+ },
+ {
+ "at_seconds": 149.032,
+ "interval_seconds": 5.015000000000015,
+ "spans": 57530,
+ "logs": 168694,
+ "metrics": 171286
+ },
+ {
+ "at_seconds": 150.05,
+ "interval_seconds": 5.035000000000025,
+ "spans": 47769,
+ "logs": 156107,
+ "metrics": 156902
+ },
+ {
+ "at_seconds": 151.086,
+ "interval_seconds": 5.041000000000025,
+ "spans": 46324,
+ "logs": 172188,
+ "metrics": 174569
+ },
+ {
+ "at_seconds": 152.018,
+ "interval_seconds": 5.973000000000013,
+ "spans": 47132,
+ "logs": 185501,
+ "metrics": 187176
+ },
+ {
+ "at_seconds": 153.036,
+ "interval_seconds": 5.007000000000005,
+ "spans": 60117,
+ "logs": 212502,
+ "metrics": 211903
+ },
+ {
+ "at_seconds": 154.017,
+ "interval_seconds": 5.9879999999999995,
+ "spans": 67135,
+ "logs": 220107,
+ "metrics": 219272
+ },
+ {
+ "at_seconds": 155.022,
+ "interval_seconds": 5.989999999999981,
+ "spans": 76462,
+ "logs": 228881,
+ "metrics": 227546
+ },
+ {
+ "at_seconds": 156.018,
+ "interval_seconds": 5.967999999999989,
+ "spans": 87635,
+ "logs": 243968,
+ "metrics": 241622
+ },
+ {
+ "at_seconds": 157.014,
+ "interval_seconds": 5.927999999999997,
+ "spans": 92443,
+ "logs": 252193,
+ "metrics": 249494
+ },
+ {
+ "at_seconds": 158.063,
+ "interval_seconds": 5.026999999999987,
+ "spans": 87328,
+ "logs": 228765,
+ "metrics": 227969
+ },
+ {
+ "at_seconds": 159.073,
+ "interval_seconds": 5.056000000000012,
+ "spans": 73180,
+ "logs": 187896,
+ "metrics": 189478
+ },
+ {
+ "at_seconds": 160.022,
+ "interval_seconds": 5,
+ "spans": 56006,
+ "logs": 194600,
+ "metrics": 196600
+ },
+ {
+ "at_seconds": 161.014,
+ "interval_seconds": 5.992000000000019,
+ "spans": 54250,
+ "logs": 176736,
+ "metrics": 180073
+ },
+ {
+ "at_seconds": 162.039,
+ "interval_seconds": 5.024999999999977,
+ "spans": 31655,
+ "logs": 147065,
+ "metrics": 153433
+ },
+ {
+ "at_seconds": 163.033,
+ "interval_seconds": 6.018999999999977,
+ "spans": 43209,
+ "logs": 157834,
+ "metrics": 160492
+ },
+ {
+ "at_seconds": 164.018,
+ "interval_seconds": 5.9550000000000125,
+ "spans": 56939,
+ "logs": 176994,
+ "metrics": 178170
+ },
+ {
+ "at_seconds": 165.016,
+ "interval_seconds": 5.942999999999984,
+ "spans": 62775,
+ "logs": 213360,
+ "metrics": 212855
+ },
+ {
+ "at_seconds": 166.931,
+ "interval_seconds": 5.917000000000002,
+ "spans": 69631,
+ "logs": 172892,
+ "metrics": 174751
+ },
+ {
+ "at_seconds": 167.028,
+ "interval_seconds": 6.013999999999982,
+ "spans": 68508,
+ "logs": 178251,
+ "metrics": 177419
+ },
+ {
+ "at_seconds": 168.112,
+ "interval_seconds": 5.079000000000008,
+ "spans": 57500,
+ "logs": 176019,
+ "metrics": 180547
+ },
+ {
+ "at_seconds": 169.045,
+ "interval_seconds": 5.026999999999987,
+ "spans": 37612,
+ "logs": 161926,
+ "metrics": 163119
+ },
+ {
+ "at_seconds": 170.016,
+ "interval_seconds": 5,
+ "spans": 34415,
+ "logs": 160400,
+ "metrics": 161600
+ },
+ {
+ "at_seconds": 171.027,
+ "interval_seconds": 6.010999999999996,
+ "spans": 43102,
+ "logs": 166362,
+ "metrics": 168691
+ },
+ {
+ "at_seconds": 172.193,
+ "interval_seconds": 5.1650000000000205,
+ "spans": 49774,
+ "logs": 187609,
+ "metrics": 190513
+ },
+ {
+ "at_seconds": 173.787,
+ "interval_seconds": 5.675000000000011,
+ "spans": 54105,
+ "logs": 179736,
+ "metrics": 179207
+ },
+ {
+ "at_seconds": 174.058,
+ "interval_seconds": 5.013000000000005,
+ "spans": 58450,
+ "logs": 168562,
+ "metrics": 171155
+ },
+ {
+ "at_seconds": 175.04,
+ "interval_seconds": 5.024000000000001,
+ "spans": 55138,
+ "logs": 168193,
+ "metrics": 170780
+ },
+ {
+ "at_seconds": 176.05,
+ "interval_seconds": 5.023000000000025,
+ "spans": 42612,
+ "logs": 132391,
+ "metrics": 136174
+ },
+ {
+ "at_seconds": 177.037,
+ "interval_seconds": 6.010000000000019,
+ "spans": 42436,
+ "logs": 150083,
+ "metrics": 151581
+ },
+ {
+ "at_seconds": 178.03,
+ "interval_seconds": 5.836999999999989,
+ "spans": 31364,
+ "logs": 149734,
+ "metrics": 154017
+ },
+ {
+ "at_seconds": 179.714,
+ "interval_seconds": 5.656000000000006,
+ "spans": 36082,
+ "logs": 154173,
+ "metrics": 157178
+ },
+ {
+ "at_seconds": 180.034,
+ "interval_seconds": 5.975999999999999,
+ "spans": 36995,
+ "logs": 154116,
+ "metrics": 159639
+ },
+ {
+ "at_seconds": 181.057,
+ "interval_seconds": 5.006999999999977,
+ "spans": 38957,
+ "logs": 178550,
+ "metrics": 182544
+ },
+ {
+ "at_seconds": 182.122,
+ "interval_seconds": 5.085000000000008,
+ "spans": 36590,
+ "logs": 154966,
+ "metrics": 162045
+ },
+ {
+ "at_seconds": 183.078,
+ "interval_seconds": 5.048000000000002,
+ "spans": 39827,
+ "logs": 169770,
+ "metrics": 175317
+ },
+ {
+ "at_seconds": 184.021,
+ "interval_seconds": 5.9909999999999854,
+ "spans": 39612,
+ "logs": 181940,
+ "metrics": 185111
+ },
+ {
+ "at_seconds": 185.027,
+ "interval_seconds": 5.312999999999988,
+ "spans": 48805,
+ "logs": 191982,
+ "metrics": 195746
+ },
+ {
+ "at_seconds": 186.019,
+ "interval_seconds": 5.985000000000014,
+ "spans": 56859,
+ "logs": 198663,
+ "metrics": 198162
+ },
+ {
+ "at_seconds": 187.019,
+ "interval_seconds": 5.9620000000000175,
+ "spans": 69823,
+ "logs": 201610,
+ "metrics": 200772
+ },
+ {
+ "at_seconds": 188.026,
+ "interval_seconds": 5.903999999999996,
+ "spans": 75929,
+ "logs": 208672,
+ "metrics": 210366
+ },
+ {
+ "at_seconds": 189.055,
+ "interval_seconds": 5.03400000000002,
+ "spans": 93564,
+ "logs": 213945,
+ "metrics": 215534
+ },
+ {
+ "at_seconds": 190.035,
+ "interval_seconds": 5.00800000000001,
+ "spans": 92252,
+ "logs": 229433,
+ "metrics": 232428
+ },
+ {
+ "at_seconds": 191.046,
+ "interval_seconds": 5.026999999999987,
+ "spans": 89119,
+ "logs": 235528,
+ "metrics": 240501
+ },
+ {
+ "at_seconds": 192.158,
+ "interval_seconds": 5.138999999999982,
+ "spans": 78809,
+ "logs": 211909,
+ "metrics": 210547
+ },
+ {
+ "at_seconds": 193.095,
+ "interval_seconds": 5.068999999999988,
+ "spans": 67863,
+ "logs": 201223,
+ "metrics": 202210
+ },
+ {
+ "at_seconds": 194.044,
+ "interval_seconds": 6.018000000000001,
+ "spans": 62313,
+ "logs": 203556,
+ "metrics": 202060
+ },
+ {
+ "at_seconds": 195.025,
+ "interval_seconds": 5.969999999999999,
+ "spans": 55449,
+ "logs": 189615,
+ "metrics": 184422
+ },
+ {
+ "at_seconds": 196.014,
+ "interval_seconds": 5.979000000000013,
+ "spans": 47008,
+ "logs": 189329,
+ "metrics": 184145
+ },
+ {
+ "at_seconds": 197.125,
+ "interval_seconds": 6.079000000000008,
+ "spans": 44754,
+ "logs": 160717,
+ "metrics": 157098
+ },
+ {
+ "at_seconds": 198.048,
+ "interval_seconds": 5.890000000000015,
+ "spans": 39059,
+ "logs": 155178,
+ "metrics": 156197
+ },
+ {
+ "at_seconds": 199.069,
+ "interval_seconds": 5.024999999999977,
+ "spans": 43402,
+ "logs": 163582,
+ "metrics": 161194
+ },
+ {
+ "at_seconds": 200.419,
+ "interval_seconds": 5.3940000000000055,
+ "spans": 42096,
+ "logs": 160549,
+ "metrics": 160734
+ },
+ {
+ "at_seconds": 201.141,
+ "interval_seconds": 5.126999999999981,
+ "spans": 39602,
+ "logs": 139653,
+ "metrics": 142188
+ },
+ {
+ "at_seconds": 202.018,
+ "interval_seconds": 6.003999999999991,
+ "spans": 43526,
+ "logs": 161059,
+ "metrics": 163891
+ },
+ {
+ "at_seconds": 203.024,
+ "interval_seconds": 5.899000000000001,
+ "spans": 41758,
+ "logs": 181217,
+ "metrics": 178335
+ },
+ {
+ "at_seconds": 204.083,
+ "interval_seconds": 5.01400000000001,
+ "spans": 49320,
+ "logs": 183087,
+ "metrics": 180495
+ },
+ {
+ "at_seconds": 205.151,
+ "interval_seconds": 6.082000000000022,
+ "spans": 45921,
+ "logs": 160802,
+ "metrics": 160638
+ },
+ {
+ "at_seconds": 206.024,
+ "interval_seconds": 5.60499999999999,
+ "spans": 42168,
+ "logs": 183586,
+ "metrics": 184657
+ },
+ {
+ "at_seconds": 207.044,
+ "interval_seconds": 5.0260000000000105,
+ "spans": 37218,
+ "logs": 149622,
+ "metrics": 147831
+ },
+ {
+ "at_seconds": 208.017,
+ "interval_seconds": 5.998999999999995,
+ "spans": 47018,
+ "logs": 168028,
+ "metrics": 166528
+ },
+ {
+ "at_seconds": 209.015,
+ "interval_seconds": 5.9909999999999854,
+ "spans": 49083,
+ "logs": 177600,
+ "metrics": 179770
+ },
+ {
+ "at_seconds": 210.021,
+ "interval_seconds": 5.937999999999988,
+ "spans": 52890,
+ "logs": 184406,
+ "metrics": 186932
+ },
+ {
+ "at_seconds": 211.036,
+ "interval_seconds": 5.0120000000000005,
+ "spans": 74222,
+ "logs": 192937,
+ "metrics": 190742
+ },
+ {
+ "at_seconds": 212.02,
+ "interval_seconds": 5.996000000000009,
+ "spans": 79720,
+ "logs": 200467,
+ "metrics": 196965
+ },
+ {
+ "at_seconds": 213.017,
+ "interval_seconds": 5,
+ "spans": 83400,
+ "logs": 221600,
+ "metrics": 217200
+ },
+ {
+ "at_seconds": 214.019,
+ "interval_seconds": 5.004000000000019,
+ "spans": 84133,
+ "logs": 194045,
+ "metrics": 190847
+ },
+ {
+ "at_seconds": 215.018,
+ "interval_seconds": 6.003000000000014,
+ "spans": 86124,
+ "logs": 206064,
+ "metrics": 201732
+ },
+ {
+ "at_seconds": 216.038,
+ "interval_seconds": 5.0020000000000095,
+ "spans": 87765,
+ "logs": 217313,
+ "metrics": 216713
+ },
+ {
+ "at_seconds": 218.403,
+ "interval_seconds": 5.385999999999996,
+ "spans": 60156,
+ "logs": 150761,
+ "metrics": 147791
+ },
+ {
+ "at_seconds": 218.448,
+ "interval_seconds": 5.431000000000012,
+ "spans": 64077,
+ "logs": 176395,
+ "metrics": 176027
+ },
+ {
+ "at_seconds": 219.1,
+ "interval_seconds": 5.080999999999989,
+ "spans": 57670,
+ "logs": 192482,
+ "metrics": 189136
+ },
+ {
+ "at_seconds": 220.054,
+ "interval_seconds": 5.036000000000001,
+ "spans": 42305,
+ "logs": 160048,
+ "metrics": 157863
+ },
+ {
+ "at_seconds": 221.094,
+ "interval_seconds": 5.055999999999983,
+ "spans": 34230,
+ "logs": 165546,
+ "metrics": 161195
+ },
+ {
+ "at_seconds": 222.015,
+ "interval_seconds": 5.976999999999975,
+ "spans": 42675,
+ "logs": 176845,
+ "metrics": 172327
+ },
+ {
+ "at_seconds": 223.018,
+ "interval_seconds": 6.97999999999999,
+ "spans": 44136,
+ "logs": 174355,
+ "metrics": 172063
+ },
+ {
+ "at_seconds": 224.016,
+ "interval_seconds": 5.567999999999984,
+ "spans": 56944,
+ "logs": 202047,
+ "metrics": 200790
+ },
+ {
+ "at_seconds": 225.014,
+ "interval_seconds": 5.914000000000016,
+ "spans": 66291,
+ "logs": 209841,
+ "metrics": 210348
+ },
+ {
+ "at_seconds": 226.015,
+ "interval_seconds": 5.960999999999984,
+ "spans": 79184,
+ "logs": 229995,
+ "metrics": 229827
+ },
+ {
+ "at_seconds": 227.022,
+ "interval_seconds": 5.007000000000005,
+ "spans": 87677,
+ "logs": 215498,
+ "metrics": 214100
+ },
+ {
+ "at_seconds": 228.76,
+ "interval_seconds": 5.74199999999999,
+ "spans": 83769,
+ "logs": 192616,
+ "metrics": 186172
+ },
+ {
+ "at_seconds": 229.106,
+ "interval_seconds": 5.090000000000003,
+ "spans": 78389,
+ "logs": 171316,
+ "metrics": 163654
+ },
+ {
+ "at_seconds": 230.029,
+ "interval_seconds": 5.014999999999986,
+ "spans": 67202,
+ "logs": 169691,
+ "metrics": 162512
+ },
+ {
+ "at_seconds": 231.072,
+ "interval_seconds": 5.057000000000016,
+ "spans": 53397,
+ "logs": 138422,
+ "metrics": 137829
+ },
+ {
+ "at_seconds": 232.015,
+ "interval_seconds": 6,
+ "spans": 55842,
+ "logs": 162500,
+ "metrics": 161833
+ },
+ {
+ "at_seconds": 233.015,
+ "interval_seconds": 5.992999999999995,
+ "spans": 55073,
+ "logs": 170532,
+ "metrics": 170866
+ },
+ {
+ "at_seconds": 234.081,
+ "interval_seconds": 5.320999999999998,
+ "spans": 68794,
+ "logs": 195640,
+ "metrics": 199587
+ },
+ {
+ "at_seconds": 235.017,
+ "interval_seconds": 5.911000000000001,
+ "spans": 82905,
+ "logs": 212654,
+ "metrics": 215192
+ },
+ {
+ "at_seconds": 236.246,
+ "interval_seconds": 5.174000000000007,
+ "spans": 96835,
+ "logs": 210862,
+ "metrics": 206997
+ },
+ {
+ "at_seconds": 237.024,
+ "interval_seconds": 5.0090000000000146,
+ "spans": 93432,
+ "logs": 199441,
+ "metrics": 196047
+ },
+ {
+ "at_seconds": 238.062,
+ "interval_seconds": 5.0470000000000255,
+ "spans": 79257,
+ "logs": 174757,
+ "metrics": 174559
+ },
+ {
+ "at_seconds": 239.121,
+ "interval_seconds": 5.0400000000000205,
+ "spans": 64489,
+ "logs": 180556,
+ "metrics": 180159
+ },
+ {
+ "at_seconds": 240.04,
+ "interval_seconds": 5.022999999999996,
+ "spans": 44799,
+ "logs": 163448,
+ "metrics": 168624
+ },
+ {
+ "at_seconds": 241.018,
+ "interval_seconds": 6.001000000000005,
+ "spans": 41169,
+ "logs": 158140,
+ "metrics": 159807
+ },
+ {
+ "at_seconds": 242.031,
+ "interval_seconds": 5.007000000000005,
+ "spans": 40554,
+ "logs": 175954,
+ "metrics": 178750
+ },
+ {
+ "at_seconds": 243.063,
+ "interval_seconds": 5.000999999999976,
+ "spans": 40801,
+ "logs": 161168,
+ "metrics": 159568
+ },
+ {
+ "at_seconds": 244.015,
+ "interval_seconds": 5.9529999999999745,
+ "spans": 42177,
+ "logs": 178397,
+ "metrics": 177054
+ },
+ {
+ "at_seconds": 245.014,
+ "interval_seconds": 5.893000000000001,
+ "spans": 37683,
+ "logs": 140506,
+ "metrics": 139318
+ },
+ {
+ "at_seconds": 246.015,
+ "interval_seconds": 5.974999999999994,
+ "spans": 31810,
+ "logs": 113640,
+ "metrics": 109121
+ },
+ {
+ "at_seconds": 247.12,
+ "interval_seconds": 5.088999999999999,
+ "spans": 27124,
+ "logs": 78797,
+ "metrics": 77815
+ },
+ {
+ "at_seconds": 248.34,
+ "interval_seconds": 5.277000000000015,
+ "spans": 29198,
+ "logs": 105173,
+ "metrics": 105552
+ },
+ {
+ "at_seconds": 249.043,
+ "interval_seconds": 5.02800000000002,
+ "spans": 24074,
+ "logs": 71798,
+ "metrics": 73588
+ },
+ {
+ "at_seconds": 250.014,
+ "interval_seconds": 5,
+ "spans": 32016,
+ "logs": 123400,
+ "metrics": 125000
+ },
+ {
+ "at_seconds": 251.034,
+ "interval_seconds": 5.0190000000000055,
+ "spans": 32094,
+ "logs": 132497,
+ "metrics": 137677
+ },
+ {
+ "at_seconds": 252.014,
+ "interval_seconds": 5.999000000000024,
+ "spans": 33360,
+ "logs": 153359,
+ "metrics": 157526
+ },
+ {
+ "at_seconds": 253.125,
+ "interval_seconds": 6.0049999999999955,
+ "spans": 38489,
+ "logs": 151707,
+ "metrics": 155537
+ },
+ {
+ "at_seconds": 254.113,
+ "interval_seconds": 5.069999999999993,
+ "spans": 36506,
+ "logs": 147929,
+ "metrics": 151085
+ },
+ {
+ "at_seconds": 255.138,
+ "interval_seconds": 5.123999999999995,
+ "spans": 31441,
+ "logs": 142077,
+ "metrics": 147541
+ },
+ {
+ "at_seconds": 256.023,
+ "interval_seconds": 6.0090000000000146,
+ "spans": 32635,
+ "logs": 136129,
+ "metrics": 139957
+ },
+ {
+ "at_seconds": 257.014,
+ "interval_seconds": 5,
+ "spans": 41012,
+ "logs": 156800,
+ "metrics": 159800
+ },
+ {
+ "at_seconds": 258.092,
+ "interval_seconds": 6.0779999999999745,
+ "spans": 48875,
+ "logs": 140342,
+ "metrics": 142646
+ },
+ {
+ "at_seconds": 259.064,
+ "interval_seconds": 5.939000000000021,
+ "spans": 47830,
+ "logs": 166526,
+ "metrics": 168210
+ },
+ {
+ "at_seconds": 260.02,
+ "interval_seconds": 5.906999999999982,
+ "spans": 45381,
+ "logs": 151515,
+ "metrics": 153885
+ },
+ {
+ "at_seconds": 261.025,
+ "interval_seconds": 5.001999999999953,
+ "spans": 44584,
+ "logs": 162135,
+ "metrics": 165534
+ },
+ {
+ "at_seconds": 262.298,
+ "interval_seconds": 5.283999999999992,
+ "spans": 41453,
+ "logs": 121499,
+ "metrics": 126041
+ },
+ {
+ "at_seconds": 263.018,
+ "interval_seconds": 6.003999999999962,
+ "spans": 39152,
+ "logs": 142405,
+ "metrics": 143238
+ },
+ {
+ "at_seconds": 264.039,
+ "interval_seconds": 5.947000000000003,
+ "spans": 37509,
+ "logs": 154532,
+ "metrics": 157054
+ },
+ {
+ "at_seconds": 265.076,
+ "interval_seconds": 5.05600000000004,
+ "spans": 50646,
+ "logs": 172271,
+ "metrics": 178600
+ },
+ {
+ "at_seconds": 266.018,
+ "interval_seconds": 5.9979999999999905,
+ "spans": 59864,
+ "logs": 188396,
+ "metrics": 193898
+ },
+ {
+ "at_seconds": 267.018,
+ "interval_seconds": 5.992999999999995,
+ "spans": 66255,
+ "logs": 173369,
+ "metrics": 177874
+ },
+ {
+ "at_seconds": 268.044,
+ "interval_seconds": 5.0260000000000105,
+ "spans": 81178,
+ "logs": 170314,
+ "metrics": 173498
+ },
+ {
+ "at_seconds": 269.017,
+ "interval_seconds": 5.999000000000024,
+ "spans": 82516,
+ "logs": 185364,
+ "metrics": 188031
+ },
+ {
+ "at_seconds": 270.015,
+ "interval_seconds": 5.975999999999999,
+ "spans": 86348,
+ "logs": 206995,
+ "metrics": 208333
+ },
+ {
+ "at_seconds": 271.119,
+ "interval_seconds": 5.101000000000056,
+ "spans": 82928,
+ "logs": 182709,
+ "metrics": 182905
+ },
+ {
+ "at_seconds": 272.096,
+ "interval_seconds": 5.078000000000031,
+ "spans": 75820,
+ "logs": 174872,
+ "metrics": 171721
+ },
+ {
+ "at_seconds": 273.03,
+ "interval_seconds": 6.0120000000000005,
+ "spans": 66707,
+ "logs": 180140,
+ "metrics": 181138
+ },
+ {
+ "at_seconds": 274.02,
+ "interval_seconds": 5.002999999999986,
+ "spans": 56375,
+ "logs": 160504,
+ "metrics": 160504
+ },
+ {
+ "at_seconds": 275.033,
+ "interval_seconds": 5.018000000000029,
+ "spans": 43053,
+ "logs": 141092,
+ "metrics": 141491
+ },
+ {
+ "at_seconds": 276.049,
+ "interval_seconds": 6.033999999999992,
+ "spans": 55696,
+ "logs": 146337,
+ "metrics": 145177
+ },
+ {
+ "at_seconds": 277.015,
+ "interval_seconds": 5.895999999999958,
+ "spans": 52081,
+ "logs": 169607,
+ "metrics": 166554
+ },
+ {
+ "at_seconds": 278.058,
+ "interval_seconds": 5.02800000000002,
+ "spans": 74988,
+ "logs": 162888,
+ "metrics": 160302
+ },
+ {
+ "at_seconds": 279.043,
+ "interval_seconds": 5.023000000000025,
+ "spans": 91186,
+ "logs": 185347,
+ "metrics": 185945
+ },
+ {
+ "at_seconds": 280.015,
+ "interval_seconds": 5.9950000000000045,
+ "spans": 90580,
+ "logs": 192827,
+ "metrics": 192827
+ },
+ {
+ "at_seconds": 281.014,
+ "interval_seconds": 5.9809999999999945,
+ "spans": 103499,
+ "logs": 207323,
+ "metrics": 206320
+ },
+ {
+ "at_seconds": 282.095,
+ "interval_seconds": 5.080000000000041,
+ "spans": 96850,
+ "logs": 168307,
+ "metrics": 168504
+ },
+ {
+ "at_seconds": 283.073,
+ "interval_seconds": 5.014999999999986,
+ "spans": 78365,
+ "logs": 173480,
+ "metrics": 177468
+ },
+ {
+ "at_seconds": 284.031,
+ "interval_seconds": 5.973000000000013,
+ "spans": 72500,
+ "logs": 181986,
+ "metrics": 181818
+ },
+ {
+ "at_seconds": 285.101,
+ "interval_seconds": 5.086000000000013,
+ "spans": 50932,
+ "logs": 153362,
+ "metrics": 153362
+ },
+ {
+ "at_seconds": 286.024,
+ "interval_seconds": 5.009999999999991,
+ "spans": 41928,
+ "logs": 149501,
+ "metrics": 152495
+ },
+ {
+ "at_seconds": 287.039,
+ "interval_seconds": 6.024999999999977,
+ "spans": 51960,
+ "logs": 152033,
+ "metrics": 155519
+ },
+ {
+ "at_seconds": 288.024,
+ "interval_seconds": 5.928999999999974,
+ "spans": 63933,
+ "logs": 178445,
+ "metrics": 182324
+ },
+ {
+ "at_seconds": 289.12,
+ "interval_seconds": 5.088999999999999,
+ "spans": 85285,
+ "logs": 179210,
+ "metrics": 178424
+ },
+ {
+ "at_seconds": 290.083,
+ "interval_seconds": 6.052000000000021,
+ "spans": 71714,
+ "logs": 150694,
+ "metrics": 150033
+ },
+ {
+ "at_seconds": 291.076,
+ "interval_seconds": 5.052000000000021,
+ "spans": 73634,
+ "logs": 142122,
+ "metrics": 143310
+ },
+ {
+ "at_seconds": 292.037,
+ "interval_seconds": 6.012999999999977,
+ "spans": 65199,
+ "logs": 149343,
+ "metrics": 149676
+ },
+ {
+ "at_seconds": 293.015,
+ "interval_seconds": 5.975999999999999,
+ "spans": 54057,
+ "logs": 159304,
+ "metrics": 160308
+ },
+ {
+ "at_seconds": 294.108,
+ "interval_seconds": 6.084000000000003,
+ "spans": 50141,
+ "logs": 152531,
+ "metrics": 151709
+ },
+ {
+ "at_seconds": 295.034,
+ "interval_seconds": 5.913999999999987,
+ "spans": 35688,
+ "logs": 130030,
+ "metrics": 129861
+ },
+ {
+ "at_seconds": 296.043,
+ "interval_seconds": 5.9599999999999795,
+ "spans": 36755,
+ "logs": 144128,
+ "metrics": 146477
+ },
+ {
+ "at_seconds": 297.213,
+ "interval_seconds": 5.176000000000045,
+ "spans": 37879,
+ "logs": 137944,
+ "metrics": 135626
+ },
+ {
+ "at_seconds": 298.058,
+ "interval_seconds": 5.043000000000006,
+ "spans": 38283,
+ "logs": 121158,
+ "metrics": 119572
+ },
+ {
+ "at_seconds": 299.046,
+ "interval_seconds": 6.031000000000006,
+ "spans": 36325,
+ "logs": 127176,
+ "metrics": 125684
+ },
+ {
+ "at_seconds": 300.047,
+ "interval_seconds": 5.013000000000034,
+ "spans": 24553,
+ "logs": 146419,
+ "metrics": 151406
+ },
+ {
+ "at_seconds": 301.025,
+ "interval_seconds": 5.9909999999999854,
+ "spans": 20878,
+ "logs": 137540,
+ "metrics": 144717
+ },
+ {
+ "at_seconds": 302.107,
+ "interval_seconds": 6.064000000000021,
+ "spans": 27721,
+ "logs": 149201,
+ "metrics": 150582
+ },
+ {
+ "at_seconds": 303.021,
+ "interval_seconds": 5.807999999999993,
+ "spans": 29796,
+ "logs": 165045,
+ "metrics": 168436
+ }
+ ]
+};
+export const sustainedSeries = benchmark.sustainedSeries;
export type Signal = keyof typeof benchmark.sweeps;
export const signalNames: Record = {
- traces: "Traces",
- logs: "Logs",
- metrics: "Metrics",
- mixed: "Mixed",
+ traces: "Traces", logs: "Logs", metrics: "Metrics", mixed: "Mixed",
};
-export const formatRate = (value: number) => value.toLocaleString("en-US");
+export const formatRate = (value: number) => Math.round(value).toLocaleString("en-US");
export const peak = (signal: Signal) =>
- benchmark.sweeps[signal].reduce((best, point) =>
- point.median > best.median ? point : best,
- );
-export const acceptedTotal = Object.values(benchmark.accepted_total).reduce(
- (sum, value) => sum + value,
- 0,
-);
+ benchmark.sweeps[signal].reduce((best, point) => point.median > best.median ? point : best);
+export const acceptedTotal = Object.values(benchmark.accepted_total).reduce((sum, value) => sum + value, 0);
export const composition = [
{ label: "Spans", rate: benchmark.sustained.spans_per_second },
{ label: "Log records", rate: benchmark.sustained.log_records_per_second },
- {
- label: "Metric points",
- rate: benchmark.sustained.metric_points_per_second,
- },
+ { label: "Metric points", rate: benchmark.sustained.metric_points_per_second },
];
diff --git a/site/src/styles/fanout.css b/site/src/styles/fanout.css
index 9704db7e..f2262e68 100644
--- a/site/src/styles/fanout.css
+++ b/site/src/styles/fanout.css
@@ -17,7 +17,7 @@
* The accent is violet, and that is a decision worth stating because the
* obvious alternative is wrong. Three hues on this site are load-bearing:
* green, amber and red mean healthy, degraded and unhealthy inside Fanout
- * itself, and they appear in the landing page's own service table. Tinting a
+ * itself and in its product captures. Tinting a
* link green would put "green means healthy" in conflict with "green means
* interactive" on the same scroll. Violet is the one accent in the product
* palette that carries no status, so it is the one free to mean "you can click
@@ -67,8 +67,7 @@
--sl-color-black: #0b0e14;
/* Healthy, degraded, unhealthy. These three are load-bearing: they mean the
- same thing on this site as they do inside the product, and three landing
- components were each carrying their own copy of the hex. Tokens because
+ same thing on this site as they do inside the product. Tokens because
the dark values wash out on a light background — the pale green and coral
that read clearly on Ayu surfaces fall under 4.5:1 on white. */
--fo-status-ok: #7fd962;
@@ -120,6 +119,50 @@ body:has(.fo-hero) header.header .sl-flex {
justify-content: space-between;
}
+/* One rhythm for the landing page, including the gap across Starlight's hero
+ and Markdown wrappers. Keep the content edges shared by copy and captures. */
+body:has(.fo-hero) {
+ --fo-section-gap: 4rem;
+ --fo-figure-gap: 2rem;
+ --fo-capture-gap: 1.5rem;
+ --sl-content-pad-x: 2rem;
+}
+
+body:has(.fo-hero) .sl-container {
+ min-width: 0;
+ margin-inline: auto;
+}
+
+body:has(.fo-hero) :is(.main-frame, .main-pane) {
+ min-width: 0;
+}
+
+body:has(.fo-hero) .sl-container > .sl-markdown-content {
+ display: grid;
+ /* Auto-minimum tracks can grow to the captures' intrinsic image width. */
+ grid-template-columns: minmax(0, 1fr);
+ min-width: 0;
+ gap: var(--fo-section-gap);
+ margin-top: var(--fo-figure-gap);
+}
+
+body:has(.fo-hero) .sl-markdown-content > * {
+ min-width: 0;
+ margin: 0;
+}
+
+@media (max-width: 71.999rem) {
+ body:has(.fo-hero) { --fo-section-gap: 3.5rem; }
+}
+
+@media (max-width: 39.999rem) {
+ body:has(.fo-hero) {
+ --fo-section-gap: 3rem;
+ --fo-capture-gap: 1.25rem;
+ --sl-content-pad-x: 1rem;
+ }
+}
+
/* Prose uses the productive member of the same Plex family. The mono identity
remains in every heading and piece of chrome, while paragraphs and lists get
the denser word shapes and calmer rhythm of a proportional text face. */
diff --git a/ui/host/e2e/global-setup.ts b/ui/host/e2e/global-setup.ts
index 24b9113a..872e18b8 100644
--- a/ui/host/e2e/global-setup.ts
+++ b/ui/host/e2e/global-setup.ts
@@ -49,6 +49,9 @@ function seed(binary: string | undefined, base: string, tokenFile: string, log:
}
export default async function globalSetup() {
+ // External landing captures use an existing instance and an in-memory session.
+ if (process.env.FANOUT_LANDING_BASE_URL) return;
+
const dir = await mkdtemp(join(tmpdir(), "fanout-e2e-"));
let child: ChildProcess | undefined;
let seedChild: ChildProcess | undefined;
diff --git a/ui/host/e2e/landing-captures.spec.ts b/ui/host/e2e/landing-captures.spec.ts
new file mode 100644
index 00000000..a886dd57
--- /dev/null
+++ b/ui/host/e2e/landing-captures.spec.ts
@@ -0,0 +1,213 @@
+import { expect, type Locator, type Page } from "@playwright/test";
+import { mkdir } from "node:fs/promises";
+import { fileURLToPath } from "node:url";
+import { cardSelector, readyCard, settle, test } from "./accessibility-support";
+import type { DashboardSpec } from "../../panels/types";
+import type { Result, TraceDetail } from "../../contracts";
+import { compareTracePages, scoreTracePage } from "./landing-trace-support";
+
+// Use either the real seeded server or an existing instance, without intercepts.
+const externalBaseURL = process.env.FANOUT_LANDING_BASE_URL;
+test.skip(process.env.FANOUT_LANDING_CAPTURES !== "1", "set FANOUT_LANDING_CAPTURES=1 to refresh landing assets");
+test.use({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
+if (externalBaseURL) test.use({
+ baseURL: externalBaseURL,
+ storageState: async ({}, use) => {
+ const cookie = process.env.FANOUT_LANDING_SESSION_COOKIE ?? "";
+ const match = /^([!#$%&'*+.^_`|~\w-]+)=([^;\s]+)$/.exec(cookie);
+ if (!match) throw new Error("FANOUT_LANDING_SESSION_COOKIE must be a nonempty name=value session cookie");
+ const url = new URL(externalBaseURL);
+ await use({ cookies: [{
+ name: match[1], value: match[2], domain: url.hostname, path: "/",
+ expires: -1, httpOnly: true, secure: url.protocol === "https:", sameSite: "Lax",
+ }], origins: [] });
+ },
+});
+
+async function readyOverviewViewport(page: Page) {
+ // Recompute intersections on each poll without scrolling lazy panels into view.
+ await expect.poll(() => page.locator(".mantine-Paper-root[data-panel]").evaluateAll(cards => cards
+ .filter(card => {
+ const bounds = card.getBoundingClientRect();
+ return bounds.width > 0 && bounds.height > 0
+ && bounds.left < window.innerWidth && bounds.right > 0
+ && bounds.top < window.innerHeight && bounds.bottom > 0;
+ })
+ .filter(card => card.querySelector('[aria-label="Loading panel"], [aria-label="Refreshing"], [data-panel-error], [aria-label$=", refresh failed"]'))
+ .map(card => card.getAttribute("data-panel"))), {
+ message: "Every panel intersecting the overview viewport must finish loading without errors",
+ }).toEqual([]);
+}
+
+test("capture the overview, service map and trace waterfall for the landing page", async ({ page, request }, info) => {
+ // The normal matrix also includes 1100px. Write each stable asset only once.
+ test.skip(info.project.use.viewport?.width !== 1440, "landing captures use the 1440px projects");
+ const theme = info.project.use.colorScheme as "light" | "dark";
+ const directory = fileURLToPath(new URL("../../../site/src/assets/landing/", import.meta.url));
+ await mkdir(directory, { recursive: true });
+ const capture = async (view: string, element?: Locator, clip?: () => Promise<{ x: number; y: number; width: number; height: number }>) => {
+ await page.mouse.move(0, 0);
+ await page.evaluate(() => {
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
+ });
+ await settle(page);
+ // Match the existing review captures' wait for chart compiler animations.
+ await page.waitForTimeout(250);
+ if (view === "overview") await readyOverviewViewport(page);
+ else await expect((element ?? page).locator('[aria-label="Loading panel"], [aria-label="Refreshing"], [aria-label="Loading drill data"], [data-panel-error]')).toHaveCount(0);
+ const options = {
+ path: `${directory}${view}-${theme}.png`, animations: "disabled" as const, scale: "device" as const,
+ // Capture-only styling: keep keyboard focus indicators in the product.
+ style: "*:focus, *:focus-visible { outline: none !important; } [role=tooltip] { visibility: hidden !important; }",
+ };
+ if (clip) await page.screenshot({ ...options, clip: await clip() });
+ else if (element) await element.screenshot(options);
+ else await page.screenshot({ ...options, fullPage: false });
+ };
+
+ const seedWindow = externalBaseURL ? null : JSON.parse(process.env.FANOUT_E2E_PERFORMANCE_WINDOW ?? "null") as { to: string } | null;
+ if (!externalBaseURL) {
+ if (!seedWindow) throw new Error("Global setup did not provide the seed window");
+ await page.clock.setFixedTime(new Date(seedWindow.to));
+ }
+ await page.addInitScript(value => localStorage.setItem("mantine-color-scheme-value", value), theme);
+ await page.goto("/");
+ await expect(page).toHaveURL(/\/dashboards\/[^/?]+/);
+ const dashboardURL = new URL(page.url());
+ if (seedWindow) {
+ dashboardURL.searchParams.set("from", new Date(Date.parse(seedWindow.to) - 3_600_000).toISOString());
+ dashboardURL.searchParams.set("to", seedWindow.to);
+ await page.goto(dashboardURL.pathname + dashboardURL.search);
+ }
+ await expect(page.locator("html")).toHaveAttribute("data-mantine-color-scheme", theme);
+ await expect(page.getByText("System overview", { exact: true }).first()).toBeVisible();
+ await expect(page.getByRole("button", { name: `Switch to ${theme === "light" ? "dark" : "light"} theme` })).toBeVisible();
+
+ const response = await request.get(`/api${dashboardURL.pathname}`);
+ expect(response.status()).toBe(200);
+ const dashboard = await response.json() as { is_default: boolean; spec: DashboardSpec };
+ expect(dashboard.is_default).toBe(true);
+ // Saved panels load when visible. Only panels in the shot gate the overview;
+ // empty results are valid, and offscreen panels are left alone.
+ await page.evaluate(() => window.scrollTo(0, 0));
+ const firstFold = [
+ ...dashboard.spec.panels.filter(panel => panel.viz === "stat"),
+ ...dashboard.spec.panels.filter(panel => panel.viz === "timeseries").slice(0, 2),
+ ];
+ for (const panel of firstFold) await expect(page.locator(cardSelector(panel.id))).toBeInViewport({ ratio: 1 });
+ const overview = page.getByRole("main", { name: "Dashboard", exact: true });
+ await capture("overview", overview, async () => {
+ const clip = await overview.evaluate(main => {
+ const bounds = main.getBoundingClientRect();
+ const x = Math.ceil(Math.max(0, bounds.left)), y = Math.ceil(Math.max(0, bounds.top));
+ return { x, y, width: Math.floor(Math.min(window.innerWidth, bounds.right)) - x,
+ height: Math.floor(Math.min(window.innerHeight, bounds.bottom)) - y };
+ });
+ expect(clip.width).toBeGreaterThanOrEqual(clip.height);
+ expect(clip.height).toBeGreaterThan(0);
+ return clip;
+ });
+
+ const mapPanel = dashboard.spec.panels.find(panel => panel.viz === "service_map");
+ if (!mapPanel) throw new Error("System overview has no service map");
+ const map = await readyCard(page, mapPanel);
+ await map.getByRole("button", { name: `${mapPanel.title} menu`, exact: true }).click();
+ await page.getByRole("menuitem", { name: "View", exact: true }).click();
+ const fullView = page.locator("[data-panel-fullscreen]");
+ await expect(fullView).toBeVisible();
+ const fullMap = fullView.locator(cardSelector(mapPanel.id));
+ await expect(fullMap.locator('[aria-label="Loading panel"], [aria-label="Refreshing"]')).toHaveCount(0);
+ await expect(fullMap.getByRole("region", { name: /service dependency graph/ }).getByRole("button").first()).toBeVisible();
+ await settle(page);
+ await page.waitForTimeout(250);
+ const wholeGraph = fullMap.getByRole("button", { name: `Show whole ${mapPanel.title} graph`, exact: true });
+ if (await wholeGraph.isVisible()) await wholeGraph.click();
+ await expect(fullMap.getByRole("region", { name: /service dependency graph/ }).getByRole("button").first()).toBeVisible();
+ await expect(fullMap.locator("[data-panel-title]")).toHaveText(mapPanel.title);
+ await capture("service-map", fullMap, async () => {
+ // Crop the settled graph itself: resizing the card refits and recentres
+ // its contents, which can reintroduce the whitespace being removed.
+ const clip = await fullMap.getByRole("region", { name: /service dependency graph/ }).evaluate(region => {
+ const boxes = [...region.querySelectorAll("button, svg > g > path, span")]
+ .map(node => node.getBoundingClientRect()).filter(box => box.width > 0 || box.height > 0);
+ if (!boxes.length) throw new Error("The service graph has no rendered bounds");
+ const left = Math.min(...boxes.map(box => box.left)), right = Math.max(...boxes.map(box => box.right));
+ const top = Math.min(...boxes.map(box => box.top)), bottom = Math.max(...boxes.map(box => box.bottom));
+ // Keep 24 CSS pixels (48 source pixels at 2x) beyond every rendered
+ // node, connector and label. Let the graph determine the aspect ratio.
+ const x = Math.floor(left - 24), y = Math.floor(top - 24);
+ return { x, y, width: Math.ceil(right + 24) - x, height: Math.ceil(bottom + 24) - y };
+ });
+ const viewport = page.viewportSize()!;
+ expect(clip.x).toBeGreaterThanOrEqual(0); expect(clip.x + clip.width).toBeLessThanOrEqual(viewport.width);
+ expect(clip.y).toBeGreaterThanOrEqual(0); expect(clip.y + clip.height).toBeLessThanOrEqual(viewport.height);
+ expect(clip.width).toBeGreaterThanOrEqual(clip.height);
+ return clip;
+ });
+
+ await page.getByRole("button", { name: "Close panel view", exact: true }).click();
+ await expect(fullView).toHaveCount(0);
+ // The drawer needs enough vertical room for all eight visible waterfall
+ // spans, pagination and the first correlated log rows.
+ await page.setViewportSize({ width: 1440, height: 1200 });
+ // Open a service, then one of its server-returned exemplar traces.
+ const inlineMap = page.locator(cardSelector(mapPanel.id));
+ const inlineWholeGraph = inlineMap.getByRole("button", { name: `Show whole ${mapPanel.title} graph`, exact: true });
+ if (await inlineWholeGraph.isVisible()) await inlineWholeGraph.click();
+ const drawer = page.getByRole("dialog");
+ const traceRows = drawer.getByRole("button", { name: / · .* · [\d.]+ ms$/ });
+ if (externalBaseURL) {
+ const services = inlineMap.getByRole("region", { name: /service dependency graph/ }).getByRole("button");
+ const labels = await services.evaluateAll(nodes => nodes.map(node => node.getAttribute("aria-label")!));
+ // Prefer the demo's checkout path, then its frontend, before other services.
+ const priority = (label: string) => /checkout/i.test(label.split(",")[0]) ? 0 : /frontend/i.test(label.split(",")[0]) ? 1 : 2;
+ labels.sort((a, b) => priority(a) - priority(b) || a.localeCompare(b));
+ if (!labels.length) throw new Error("The live service map has no services");
+ const exemplarsReady = page.waitForResponse(response => new URL(response.url()).pathname === "/api/panels/exemplars" && response.request().method() === "POST");
+ await inlineMap.getByRole("button", { name: labels[0], exact: true }).click();
+ const exemplarsResponse = await exemplarsReady;
+ expect(exemplarsResponse.status()).toBe(200);
+ const { traces } = await exemplarsResponse.json() as { traces: { trace_id: string; namespace: string }[] };
+ const selection = exemplarsResponse.request().postDataJSON() as { time: { from: string; to: string } };
+ const candidates = await Promise.all(traces.map(async (trace, index) => {
+ const params = new URLSearchParams({ namespace: trace.namespace, from: selection.time.from, to: selection.time.to, limit: "200" });
+ const response = await request.get(`/api/traces/${encodeURIComponent(trace.trace_id)}?${params}`);
+ expect(response.status()).toBe(200);
+ const { data } = await response.json() as Result;
+ return { index, data };
+ }));
+ // Rank the actual first waterfall page of complete, error-free traces.
+ const ranked = candidates.flatMap(({ index, data }) => {
+ const score = scoreTracePage(data);
+ return score ? [{ index, score }] : [];
+ }).sort((a, b) => compareTracePages(a.score, b.score));
+ if (!ranked.length) throw new Error("No complete, error-free live exemplar has at least 3 services on its first waterfall page and a positive duration; send more workload before capturing");
+ await expect(drawer).toBeVisible();
+ await expect(traceRows).toHaveCount(traces.length);
+ await traceRows.nth(ranked[0].index).click();
+ } else {
+ await inlineMap.getByRole("button", { name: /^checkout, / }).click();
+ await expect(drawer).toBeVisible();
+ await traceRows.first().click();
+ }
+ await expect(drawer.getByRole("button", { name: "Waterfall", exact: true })).toHaveAttribute("aria-pressed", "true");
+ // Capture-only width gives the waterfall and logs a landscape frame without
+ // changing the product drawer or shrinking its text to fit a portrait crop.
+ await drawer.evaluate(dialog => Object.assign(dialog.style, { flex: "0 0 1200px", width: "1200px", maxWidth: "100vw" }));
+ await settle(page);
+ await expect(drawer.getByRole("columnheader", { name: "Timeline", exact: true })).toBeVisible();
+ await expect(drawer.getByRole("img", { name: / took / }).first()).toBeVisible();
+ await expect(drawer.getByRole("img", { name: / took / }).last()).toBeInViewport({ ratio: 1 });
+ await expect(drawer.getByText("Correlated logs", { exact: true })).toBeVisible();
+ const logs = drawer.locator("tbody tr").filter({ has: page.locator("[data-field=body]") });
+ await expect(logs.first()).toBeVisible();
+ const lastLog = logs.nth(Math.min(2, await logs.count() - 1));
+ await expect(lastLog).toBeInViewport({ ratio: 1 });
+ await expect(drawer.getByRole("alert")).toHaveCount(0);
+ await capture("trace", drawer, async () => {
+ const dialog = (await drawer.boundingBox())!, last = (await lastLog.boundingBox())!;
+ const clip = { x: dialog.x, y: dialog.y, width: dialog.width, height: Math.ceil(last.y + last.height - dialog.y) };
+ expect(clip.width).toBeGreaterThanOrEqual(clip.height);
+ return clip;
+ });
+});
diff --git a/ui/host/e2e/landing-trace-support.test.ts b/ui/host/e2e/landing-trace-support.test.ts
new file mode 100644
index 00000000..0ac3255f
--- /dev/null
+++ b/ui/host/e2e/landing-trace-support.test.ts
@@ -0,0 +1,80 @@
+import { expect, it } from "vitest";
+import type { TraceDetail, TraceSpan } from "../../contracts";
+import { pageItems, TRACE_PAGE_SIZE } from "../src/dashboards/trace/pagination";
+import { compareTracePages, scoreTracePage } from "./landing-trace-support";
+
+function span(id: string, start: number, duration: number, service = id): TraceSpan {
+ return {
+ span_id: id, service, operation: id, kind: "SERVER", status: "OK",
+ start: new Date(Date.UTC(2026, 9, 1) + start).toISOString(), duration_ms: duration,
+ };
+}
+
+function trace(spans: TraceSpan[], overrides: Partial = {}): TraceDetail {
+ const services = [...new Set(spans.map(span => span.service))];
+ return {
+ trace_id: "trace", duration_ms: 100, has_error: false, services, spans, logs: [],
+ span_count: spans.length, service_count: services.length, truncated: false, ...overrides,
+ };
+}
+
+it("scores the union of overlapping intervals without counting gaps or nesting twice", () => {
+ const data = trace([span("a", 10, 30), span("b", 0, 20), span("c", 70, 10), span("d", 12, 3)]);
+ const original = [...data.spans];
+ expect(scoreTracePage(data)).toEqual({
+ traceID: "trace", coverage: 0.5, startSpread: 0.7, serviceCount: 4, fitsOnePage: true,
+ });
+ expect(data.spans).toEqual(original);
+});
+
+it("uses the UI's first page in server order, even when later spans start earlier", () => {
+ const spans = Array.from({ length: TRACE_PAGE_SIZE }, (_, i) => span(`first-${i}`, 40 + i, 10, `service-${i % 3}`));
+ spans.push(span("later", 0, 100));
+ expect(pageItems(spans)).toEqual(spans.slice(0, TRACE_PAGE_SIZE));
+ expect(pageItems(spans, 2)).toEqual([spans.at(-1)]);
+ expect(scoreTracePage(trace(spans))).toEqual({
+ traceID: "trace", coverage: 0.17, startSpread: 0.07, serviceCount: 3, fitsOnePage: false,
+ });
+ const tooFewServices = spans.map((item, i) => i < TRACE_PAGE_SIZE ? { ...item, service: "only" } : item);
+ expect(scoreTracePage(trace(tooFewServices))).toBeNull();
+});
+
+it("normalizes coverage and start spread by trace duration", () => {
+ const data = trace([span("a", 0, 20), span("b", 40, 10), span("c", 90, 10)]);
+ const scaled = trace([span("a", 0, 200), span("b", 400, 100), span("c", 900, 100)], { duration_ms: 1000 });
+ expect(scoreTracePage(data)).toEqual(scoreTracePage(scaled));
+});
+
+it("prefers a distributed single-page trace over a long root and seven early slivers", () => {
+ const clustered = [span("root", 0, 834), ...Array.from({ length: 7 }, (_, i) => span(`early-${i}`, i + 1, 1))];
+ clustered.push(...Array.from({ length: 186 }, (_, i) => span(`later-${i}`, 100 + i, 1)));
+ const busy = scoreTracePage(trace(clustered, { trace_id: "large", duration_ms: 834 }))!;
+ const distributed = scoreTracePage(trace([span("a", 0, 100), span("b", 30, 30), span("c", 70, 30)]))!;
+ expect(busy.coverage).toBe(1);
+ expect(distributed.coverage).toBe(1);
+ expect(compareTracePages(distributed, busy)).toBeLessThan(0);
+});
+
+it("rejects truncated, incomplete and erroneous traces, including errors beyond page one", () => {
+ const spans = [span("a", 0, 100), span("b", 30, 30), span("c", 70, 30)];
+ for (const overrides of [{ truncated: true }, { span_count: 4 }, { has_error: true }, { duration_ms: 0 }]) {
+ expect(scoreTracePage(trace(spans, overrides))).toBeNull();
+ }
+ const withError = [...spans, ...Array.from({ length: TRACE_PAGE_SIZE }, (_, i) => span(`extra-${i}`, 1, 1))];
+ withError[withError.length - 1].status = "STATUS_CODE_ERROR";
+ expect(scoreTracePage(trace(withError))).toBeNull();
+});
+
+it("prefers one page on equal visual scores, then page services and trace ID", () => {
+ const spans = [span("a", 0, 100), span("b", 30, 30), span("c", 70, 30)];
+ const single = scoreTracePage(trace(spans))!;
+ const multiple = scoreTracePage(trace([...spans, ...Array.from({ length: TRACE_PAGE_SIZE }, (_, i) => span(`extra-${i}`, 30, 1))]))!;
+ expect(single.coverage + single.startSpread).toBe(multiple.coverage + multiple.startSpread);
+ expect(compareTracePages(single, multiple)).toBeLessThan(0);
+ const moreServices = scoreTracePage(trace([...spans, span("d", 40, 1)]))!;
+ expect(compareTracePages(moreServices, single)).toBeLessThan(0);
+ const earlierID = scoreTracePage(trace(spans, { trace_id: "aaa" }))!;
+ const laterID = scoreTracePage(trace(spans, { trace_id: "zzz" }))!;
+ expect([laterID, earlierID].sort(compareTracePages)).toEqual([earlierID, laterID]);
+ expect(compareTracePages(single, single)).toBe(0);
+});
diff --git a/ui/host/e2e/landing-trace-support.ts b/ui/host/e2e/landing-trace-support.ts
new file mode 100644
index 00000000..4196d211
--- /dev/null
+++ b/ui/host/e2e/landing-trace-support.ts
@@ -0,0 +1,47 @@
+import type { TraceDetail } from "../../contracts";
+import { pageItems } from "../src/dashboards/trace/pagination";
+
+interface TracePageScore {
+ traceID: string;
+ coverage: number;
+ startSpread: number;
+ serviceCount: number;
+ fitsOnePage: boolean;
+}
+
+export function scoreTracePage(trace: TraceDetail): TracePageScore | null {
+ if (trace.truncated || trace.spans.length !== trace.span_count || trace.has_error
+ || trace.spans.some(span => span.status.toUpperCase().includes("ERROR"))
+ || !Number.isFinite(trace.duration_ms) || trace.duration_ms <= 0) return null;
+
+ const firstPage = pageItems(trace.spans);
+ const serviceCount = new Set(firstPage.map(span => span.service)).size;
+ if (serviceCount < 3) return null;
+
+ const traceStart = Math.min(...trace.spans.map(span => new Date(span.start).valueOf()));
+ const intervals = firstPage.map(span => {
+ const start = new Date(span.start).valueOf() - traceStart;
+ return { start, end: start + span.duration_ms };
+ }).sort((a, b) => a.start - b.start);
+ let covered = 0, coveredUntil = 0;
+ for (const { start, end } of intervals) {
+ covered += Math.max(0, end - Math.max(start, coveredUntil));
+ coveredUntil = Math.max(coveredUntil, end);
+ }
+ return {
+ traceID: trace.trace_id,
+ coverage: covered / trace.duration_ms,
+ startSpread: (intervals.at(-1)!.start - intervals[0].start) / trace.duration_ms,
+ serviceCount,
+ fitsOnePage: firstPage.length === trace.spans.length,
+ };
+}
+
+export function compareTracePages(a: TracePageScore, b: TracePageScore): number {
+ // Equal weight for occupied time and distributed starts: a long root alone
+ // can fill the timeline while the other seven bars cluster at its left edge.
+ return (b.coverage + b.startSpread) - (a.coverage + a.startSpread)
+ || Number(b.fitsOnePage) - Number(a.fitsOnePage)
+ || b.serviceCount - a.serviceCount
+ || a.traceID.localeCompare(b.traceID);
+}
diff --git a/ui/host/src/dashboards/trace/components.tsx b/ui/host/src/dashboards/trace/components.tsx
index b5a4b37e..1bea5148 100644
--- a/ui/host/src/dashboards/trace/components.tsx
+++ b/ui/host/src/dashboards/trace/components.tsx
@@ -1,5 +1,6 @@
import { Button, Center, Group, Text, ThemeIcon, Box } from "@mantine/core";
import { useEffect, useState, type ReactNode } from "react";
+import { pageItems, TRACE_PAGE_SIZE } from "./pagination";
export function EmptyState({
icon,
title,
@@ -29,7 +30,7 @@ export function EmptyState({
);
}
-export function usePagedItems(items: T[], pageSize = 8) {
+export function usePagedItems(items: T[], pageSize = TRACE_PAGE_SIZE) {
const [page, setPage] = useState(1);
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
useEffect(() => {
@@ -40,7 +41,7 @@ export function usePagedItems(items: T[], pageSize = 8) {
page,
setPage,
totalPages,
- pageItems: items.slice(start, start + pageSize),
+ pageItems: pageItems(items, page, pageSize),
from: items.length ? start + 1 : 0,
to: Math.min(start + pageSize, items.length),
total: items.length,
diff --git a/ui/host/src/dashboards/trace/pagination.ts b/ui/host/src/dashboards/trace/pagination.ts
new file mode 100644
index 00000000..a68d4c33
--- /dev/null
+++ b/ui/host/src/dashboards/trace/pagination.ts
@@ -0,0 +1,7 @@
+export const TRACE_PAGE_SIZE = 8;
+
+export function pageItems(items: T[], page = 1, pageSize = TRACE_PAGE_SIZE): T[] {
+ // Waterfall rows keep the server's span order, including on the first page.
+ const start = (page - 1) * pageSize;
+ return items.slice(start, start + pageSize);
+}
diff --git a/ui/host/src/dashboards/trace/trace-components.tsx b/ui/host/src/dashboards/trace/trace-components.tsx
index 264ad9be..f20755af 100644
--- a/ui/host/src/dashboards/trace/trace-components.tsx
+++ b/ui/host/src/dashboards/trace/trace-components.tsx
@@ -20,7 +20,7 @@ export function Waterfall({
);
const total = Math.max(end - start, 1);
const services = [...new Set(spans.map(span => span.service))];
- const visibleSpans = usePagedItems(spans, 8);
+ const visibleSpans = usePagedItems(spans);
return (
<>