Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion apps/web/src/lab/bench/chart-bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { defineLabWithFlags, type LabEntry } from "../define"
import * as Chart from "@maple/views/charts/chart"
import { ChartRuntime, isChartRuntimeReady, LoadChartRuntime } from "@maple/views/charts/chart-runtime"
import type { Resources } from "../../platform/resources"
import * as HoverCard from "@maple/views/ui/hover-card"
import { initMarkerCard } from "../../page/kit/service-metrics/marker-card"

// The chart benches' shared state: a canvas chart per id,
// the chart runtime, the resolved theme, one linked cursor across them, and the span a drag zoomed to.
Expand All @@ -17,6 +19,8 @@ export const Model = Schema.Struct({
charts: Schema.Record(Schema.String, Chart.Model),
/** The span the last drag selected; a page writes it to its URL, a bench narrows its rows. */
zoom: Schema.NullOr(Schema.Struct({ startMs: Schema.Number, endMs: Schema.Number })),
/** The commit markers' hover card, on the benches that draw markers. */
markerCard: HoverCard.Model,
...ChartEnvFields,
})
export type Model = typeof Model.Type
Expand All @@ -26,6 +30,7 @@ export const Message = defineMessageUnion({
...ChartEnvMessages,
CompletedMountLinkedCursor: {},
SelectedChartRange: { startMs: Schema.Number, endMs: Schema.Number },
GotMarkerCardMessage: { message: HoverCard.Message },
})
export type Message = typeof Message.Type

Expand All @@ -40,7 +45,14 @@ export const benchFlags: Effect.Effect<BenchFlags> = Effect.all({
type Return = Update.Return<Model, Message, Resources>

export const init = (flags: BenchFlags): Return => ({
model: { ...flags, charts: {}, zoom: null, chartRuntime: ChartRuntime.Loading(), themeColors: null },
model: {
...flags,
charts: {},
zoom: null,
markerCard: initMarkerCard("bench-marker-card"),
chartRuntime: ChartRuntime.Loading(),
themeColors: null,
},
commands: [LoadChartRuntime()],
})

Expand All @@ -62,6 +74,15 @@ export const update = (model: Model, message: Message): Return =>
}),
CompletedMountLinkedCursor: () => ({ model }),
SelectedChartRange: (zoom) => ({ model: { ...model, zoom } }),
GotMarkerCardMessage: ({ message: inner }) => {
const next = HoverCard.update(model.markerCard, inner)
return {
model: { ...model, markerCard: next.model },
commands: Command.mapMessages(next.commands ?? [], (card) =>
Message.GotMarkerCardMessage({ message: card }),
),
}
},
})

export const isReady = (model: Model): boolean =>
Expand Down
89 changes: 84 additions & 5 deletions apps/web/src/lab/bench/service-detail-bench.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { parseBucketMs } from "@maple/ui/lib/format"
import { Array as Arr } from "effect"
import { Array as Arr, Record as Rec, String as Str } from "effect"
import { AsyncData } from "foldkit"
import type { Html, HtmlBuilder } from "foldkit/html"
import type { FixedMetricRow } from "@maple/views/charts/chart-spec"
import { MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import { ResolveThemeColors } from "@maple/views/charts/theme-colors"
import { isReady, Message, type Model } from "./chart-bench"
import { CHART_TOKENS, metricsGridOf } from "../../page/kit/service-metrics/grid"
import { commitMarkers } from "../../page/kit/service-metrics/markers"
import type { CommitData, Release } from "../../page/kit/service-metrics/points"

// `/lab/bench/service-detail` and
// `/lab/bench/overview`): the service-detail chart grid over synthetic rows. A drag zooms the rows.
Expand All @@ -14,13 +17,16 @@ const BUCKET_COUNT = 145
const BUCKET_MS = 5 * 60 * 1000
const END_TIME_MS = Date.UTC(2026, 6, 17, 18, 30)

const bucketAt = (index: number) =>
new Date(END_TIME_MS - (BUCKET_COUNT - 1 - index) * BUCKET_MS).toISOString()

export const SERVICE_DETAIL_POINTS: ReadonlyArray<FixedMetricRow> = Arr.makeBy(BUCKET_COUNT, (index) => {
const phase = (index / (BUCKET_COUNT - 1)) * Math.PI * 6
const traffic = 1_250 + Math.sin(phase) * 410 + Math.cos(phase * 0.37) * 190
const errorRate = 0.18 + Math.max(0, Math.sin(phase * 0.73)) * 0.42
const p50LatencyMs = 320 + Math.sin(phase * 0.63) * 85
return {
bucket: new Date(END_TIME_MS - (BUCKET_COUNT - 1 - index) * BUCKET_MS).toISOString(),
bucket: bucketAt(index),
p50LatencyMs,
p95LatencyMs: p50LatencyMs * 1.8,
p99LatencyMs: p50LatencyMs * 2.75,
Expand All @@ -40,8 +46,75 @@ const rowsIn = (zoom: Model["zoom"]): ReadonlyArray<FixedMetricRow> =>
return at !== null && at >= zoom.startMs && at <= zoom.endMs
})

const sha = (seed: string) => Str.repeat(40 / seed.length)(seed)

/** Synthetic deploys: one already running, a lone deploy, a group of three and a late one. */
const DEPLOYS: ReadonlyArray<Readonly<{ at: number; sha: string; message: string; author: string }>> = [
{ at: 0, sha: sha("a0"), message: "chore: baseline", author: "baseline" },
{ at: 38, sha: sha("b1"), message: "feat(security): audit every token exchange", author: "dana" },
{
at: 82,
sha: sha("c2"),
message: "fix(ingest): keep three request costs bounded per request",
author: "lee",
},
{ at: 82, sha: sha("d3"), message: "perf(query): cache the service map rollup", author: "kai" },
{ at: 82, sha: sha("e4"), message: "refactor(api): one error shape for every route", author: "sam" },
{ at: 118, sha: sha("f5"), message: "feat(web): keyboard row navigation on lists", author: "dana" },
]

const RELEASES: ReadonlyArray<Release> = Arr.flatMap(DEPLOYS, (deploy) =>
Arr.makeBy(BUCKET_COUNT - deploy.at, (offset) => ({
bucket: bucketAt(deploy.at + offset),
commitSha: deploy.sha,
count: 100,
errorCount: 0,
})),
)

const COMMITS: Readonly<Record<string, CommitData>> = Rec.fromEntries(
Arr.map(DEPLOYS, (deploy, index) => [
deploy.sha,
AsyncData.Success({
data: {
sha: deploy.sha,
message: deploy.message,
authorName: null,
authorLogin: deploy.author,
authorAvatarUrl: null,
committedAt: END_TIME_MS - (BUCKET_COUNT - deploy.at) * BUCKET_MS - index * 60_000,
},
}),
]),
)

/** The bench page: `MetricsGrid` with `yAxisWidth={72}` under a padded full-height frame. */
export const view = (model: Model, h: HtmlBuilder<Message>): Html =>
export const view = (model: Model, h: HtmlBuilder<Message>): Html => {
const rows = rowsIn(model.zoom)
return viewOf(model, h, rows, [])
}

/** The bench with deploy markers, a group of three among them; hovering one opens its card. */
export const viewWithMarkers = (model: Model, h: HtmlBuilder<Message>): Html => {
const rows = rowsIn(model.zoom)
return viewOf(
model,
h,
rows,
commitMarkers(
RELEASES,
Arr.map(rows, (row) => String(row.bucket)),
COMMITS,
),
)
}

const viewOf = (
model: Model,
h: HtmlBuilder<Message>,
rows: ReadonlyArray<FixedMetricRow>,
markers: ReturnType<typeof commitMarkers>,
): Html =>
h.div(
[
h.DataAttribute("testid", "service-detail-chart-bench"),
Expand All @@ -53,8 +126,14 @@ export const view = (model: Model, h: HtmlBuilder<Message>): Html =>
h,
{
charts: model.charts,
rows: rowsIn(model.zoom),
markers: [],
rows,
markers,
markerCard: {
model: model.markerCard,
toParent: (message) => Message.GotMarkerCardMessage({ message }),
commits: COMMITS,
clock: { nowMs: model.nowMs, timeZone: model.timeZone },
},
toChartMessage: (chartId, message) => Message.GotChartMessage({ chartId, message }),
linkedCursorMount: MountLinkedZoom({ timeZone: model.timeZone }),
},
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/lab/entries/bench/service-detail-markers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineChartBench } from "../../bench/chart-bench"
import { viewWithMarkers } from "../../bench/service-detail-bench"

// `/lab/bench/service-detail-markers`: the service-detail grid with deploy markers, one of them grouped.

export const lab = defineChartBench("Service detail markers", viewWithMarkers)
7 changes: 7 additions & 0 deletions apps/web/src/lab/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,13 @@ export const LAB_ENTRIES: ReadonlyArray<LabEntry> = [
kind: "bench",
session: "none",
},
{
path: "/lab/bench/service-detail-markers",
title: "Service detail markers",
description: "The service-detail MetricsGrid with deploy markers; hover a label for its commit card.",
kind: "bench",
session: "none",
},
{
path: "/lab/bench/infra",
title: "Infra charts",
Expand Down
19 changes: 17 additions & 2 deletions apps/web/src/page/kit/service-metrics/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
LINKED_ZOOM_CLASS,
linkedCursorOverlay,
} from "@maple/views/charts/linked-cursor"
import type { MarkerCard } from "./marker-card"
import { markersOverlay } from "./markers-view"
import { chartFrameClass } from "@maple/views/ui/chart-frame"

Expand Down Expand Up @@ -158,6 +159,8 @@ export type GridInput<M> = Readonly<{
charts: Readonly<Record<string, Chart.Model>>
rows: ReadonlyArray<FixedMetricRow>
markers: ReturnType<typeof commitMarkers>
/** The page's marker hover card; without it the markers draw idle. */
markerCard?: MarkerCard<M>
toChartMessage: (chartId: string, message: Chart.Message) => M
/** `h.OnMount(MountLinkedZoom(...))`, lifted to the page's Message. */
linkedCursorMount: Parameters<HtmlBuilder<M>["OnMount"]>[0]
Expand All @@ -176,7 +179,17 @@ const chartBody = <M>(
? undefined
: Option.match(chartModel.plot, {
onNone: () => undefined,
onSome: (plot) => markersOverlay(h, markerLayout(markers, plot), plot),
onSome: (plot) =>
markersOverlay(
h,
markerLayout(markers, plot),
plot,
undefined,
"all",
input.markerCard === undefined
? undefined
: { ...input.markerCard, scope: `markers-${chart.id}` },
),
})
return h.div(
[h.Class("relative h-full min-h-0 w-full")],
Expand All @@ -190,11 +203,13 @@ const chartBody = <M>(
ariaLabel: chart.ariaLabel,
spec: specOf(chart, rows, context),
className: chartFrameClass("fill"),
overlay,
reportPlot: true,
},
toParentMessage: (message: Chart.Message) => input.toChartMessage(chart.id, message),
}),
// Beside the chart, not its `overlay` input: the marker card's handlers must bind to this
// page, and the plot box fills this wrapper, so the plot geometry lines up as is.
overlay ?? null,
linkedCursorOverlay(h, chart.id),
],
)
Expand Down
104 changes: 104 additions & 0 deletions apps/web/src/page/kit/service-metrics/marker-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { shortId } from "@maple/ui/lib/ids"
import { AsyncData } from "foldkit"
import type { Html, HtmlBuilder } from "foldkit/html"
import * as HoverCard from "@maple/views/ui/hover-card"
import { skeleton } from "@maple/views/ui/skeleton"
import { hstack, vstack } from "@maple/views/ui/stack"
import { typography } from "@maple/views/ui/typography"
import { Array as Arr } from "effect"
import { commitAvatar, firstLine, isResolvableSha } from "../commit-card"
import { relativeTime } from "../relative-time"
import type { MarkerCommit } from "./marker-layout"
import type { CommitData } from "./points"

// The card a commit marker opens on hover: every commit first seen in that bucket,
// the representative first.

/** A marker card lists at most this many commits; the rest are counted. */
export const MARKER_CARD_COMMITS = 8

/** One card per page, shared by every marker on every chart. */
export const initMarkerCard = (id: string): HoverCard.Model =>
HoverCard.init(id, { openDelayMs: 300, side: "bottom", align: "center", sideOffset: 6 })

/** What a markers overlay needs to open the card: the page's card, its commits and its clock. */
export type MarkerCard<M> = Readonly<{
model: HoverCard.Model
toParent: (message: HoverCard.Message) => M
commits: Readonly<Record<string, CommitData>>
clock: Readonly<{ nowMs: number; timeZone: string }>
}>

const commitRow = <M>(h: HtmlBuilder<M>, card: MarkerCard<M>, commit: MarkerCommit): Html => {
const data = card.commits[commit.sha]
const sha = isResolvableSha(commit.sha) ? shortId(commit.sha, "sha") : commit.sha
const shaText = typography(h, { size: "2xs", tone: "muted", mono: true, className: "shrink-0" }, [sha])
if (data === undefined || !AsyncData.isSuccess(data)) {
const isLoading = data !== undefined && AsyncData.isLoading(data)
return hstack(h, { gap: "2.5", className: "px-3 py-2" }, [
isLoading ? skeleton(h, "size-5 shrink-0 rounded-full") : commitAvatar(h, null, "?"),
isLoading
? skeleton(h, "h-3 flex-1")
: typography(h, { size: "xs", tone: "muted", className: "flex-1" }, [
"Commit details unavailable",
]),
shaText,
])
}
const detail = data.data
const author = detail.authorLogin ?? detail.authorName ?? "Unknown author"
return hstack(h, { gap: "2.5", align: "start", className: "px-3 py-2" }, [
commitAvatar(h, detail.authorAvatarUrl, author),
vstack(h, { gap: "0.5", shrink: true, grow: true }, [
typography(h, { size: "xs", weight: "medium", truncate: true }, [firstLine(detail.message)]),
hstack(h, { gap: "1", shrink: true }, [
typography(h, { size: "2xs", tone: "muted", truncate: true }, [author]),
typography(h, { size: "2xs", tone: "faint" }, ["·"]),
relativeTime(h, {
value: detail.committedAt,
...card.clock,
variant: "short",
className: "shrink-0 text-2xs text-muted-foreground",
}),
]),
]),
shaText,
])
}

/** The card's body for one marker group: a header, one row per commit, and the overflow count. */
export const markerCardBody = <M>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

The marker card's open path and its rows have no test

F1 · Warning · tests

markers.test.ts pins only markerShas. Nothing exercises what this PR adds: markersOverlay handing triggerFor to a label or dash, the groupOf lookup that resolves a trigger to its group, or markerCardBody listing a merged group's commits with the +N more cap. The repository scene-tests hover cards of exactly this shape.

Add a scene test: hover a grid marker label, resolve `WaitHoverDelay` and `MountHoverCard`, then assert the card lists every commit of the merged group and stops at `MARKER_CARD_COMMITS` with `+N more`.
🤖 Prompt to fix with an AI agent
In `apps/web/src/page/kit/service-metrics/marker-card.ts:70-74`: The marker card's open path and its rows have no test.

`markers.test.ts` pins only `markerShas`. Nothing exercises what this PR adds: `markersOverlay` handing `triggerFor` to a label or dash, the `groupOf` lookup that resolves a trigger to its group, or `markerCardBody` listing a merged group's commits with the `+N more` cap. The repository scene-tests hover cards of exactly this shape.

Suggested fix: Add a scene test: hover a grid marker label, resolve `WaitHoverDelay` and `MountHoverCard`, then assert the card lists every commit of the merged group and stops at `MARKER_CARD_COMMITS` with `+N more`.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

h: HtmlBuilder<M>,
card: MarkerCard<M>,
commits: ReadonlyArray<MarkerCommit>,
): ReadonlyArray<Html> => {

Check warning on line 74 in apps/web/src/page/kit/service-metrics/marker-card.ts

View check run for this annotation

Maple Review Bot / Maple / review

tests: The marker card's open path and its rows have no test

`markers.test.ts` pins only `markerShas`. Nothing exercises what this PR adds: `markersOverlay` handing `triggerFor` to a label or dash, the `groupOf` lookup that resolves a trigger to its group, or `markerCardBody` listing a merged group's commits with the `+N more` cap. The repository scene-tests hover cards of exactly this shape.
const shown = commits.slice(0, MARKER_CARD_COMMITS)
const hidden = commits.length - shown.length
const count = commits.length
return [
typography(
h,
{ size: "2xs", tone: "muted", as: "div", className: "border-b border-border-subtle px-3 py-2" },
[count === 1 ? "1 commit deployed" : `${count} commits deployed`],
),
vstack(
h,
{ className: "py-1" },
Arr.map(shown, (commit) => commitRow(h, card, commit)),
),
...(hidden > 0
? [
typography(
h,
{
size: "2xs",
tone: "muted",
as: "div",
className: "border-t border-border-subtle px-3 py-2",
},
[`+${hidden} more`],
),
]
: []),
]
}
Loading
Loading