Skip to content

console: add resource usage charts to the redesigned cluster page - #39508

Open
leedqin wants to merge 1 commit into
cluster-page-usage-datafrom
cluster-page-resource-usage
Open

leedqin wants to merge 1 commit into
cluster-page-usage-datafrom
cluster-page-resource-usage

Conversation

@leedqin

@leedqin leedqin commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Third PR in the cluster details redesign stack, on top of #39507. It replaces the Metrics tab's overview charts with the resource usage card from the design. CPU, memory and replica status share one time axis and one tooltip, so a spike, an out-of-memory kill and the restart after it line up. Fixes CNS-174

Description

  • Header: a toggle button per replica (color, name and size, with dropped replicas marked), usable from the keyboard, and the time range picker shared with the classic page. useChartLegend's toggle now takes any click event, since it only reads the modifier keys.
  • CPU: a line per replica, as a share of the replica's CPU allocation. When a reading passes 100%, the axis extends to fit it and a dashed line marks the allocation.
  • Memory: a bar per replica as a share of the heap limit (RAM plus swap), on a fixed 0 to 100% axis so sizes with a lot of swap don't stretch it. The replicas' bars at one time overlap by half a bar, so each group reads as one point in time.
    • On 26.44 and later the bar splits into RAM and swap, and the legend gives the RAM and heap limits in bytes.
    • Earlier environments show the bar as heap alone, labelled "Heap".
    • Where no sample in the window has a heap limit, such as on the local emulator, the bars are a share of the memory allocation, and the labels say so. Only the views' rows on 26.44 and later carry that, so other windows keep the heap labels.
  • Replica status: a row per replica, including one offline the whole window. Running, finished hydration and offline stretches, a marker for each out-of-memory kill, and a "Hydrating · N objects" badge at the now edge while a replica has objects left to hydrate.
  • No readings: when no sample in the window has a reading, the card says why from replica status: replicas ran but weren't measured, every replica was offline after an out-of-memory kill (with the memory troubleshooting guide), every replica was offline with no recorded reason (with the hydration troubleshooting guide), or status is unknown. Cloud adds a contact-support link; self-managed points at metrics-server. Replica status always shows below.
  • Markers: a dotted line where an index or materialized view was created on the cluster, labelled on the CPU chart. The objects come from useClusterObjectsLive, so DDL elsewhere in the environment doesn't redraw the charts. The cluster page mounts the objects collection bridge, as Object Explorer does.
  • Replicas that share a name: a scheduled cluster gets a new replica id on every refresh, under the same name. Those replicas share one toggle, one color and one status row. A time's memory bars split only among the replicas reporting at that time, and bucket merging counts the same, so a week of refreshes keeps readable lines and bars.
  • Processes: when a replica's size runs more than one process, a Processes button on the CPU chart opens a drawer with a tab per such replica. Each tab leads with when CPU and memory peaked and on which process, then charts the busiest process against the average one, as shares of one process's allocation, over the card's window up to 24h. The drawer reads each process's samples from mz_cluster_replica_metrics_history (a lookup on its replica_id index) through a SUBSCRIBE that is open only while the drawer is, starting at console: add data for the redesigned cluster page's resource usage #39507's rolling literal start.
  • Tooltip: per replica, its status and readings at the hovered time. A Swap row appears only where the size has swap.
  • Wide windows: neighboring buckets merge, keeping each metric's peak, so bars stay at least 10px wide. The time axis matches the classic graphs.
  • Metrics tab: the system cluster info box, this card, then the existing resource intensive objects and freshness cards, unchanged.

Loading and deriving live in useResourceUsageData. The chart marks are memoized, so hovering only redraws the cursor and tooltip.

The classic page's check for a cluster reporting no metrics moves into hasNoUtilizationMetrics, which both pages now use. Its behavior is unchanged.

Differences from the classic graphs: the CPU axis extends past 100% instead of clamping, there's no disk chart, replica toggles replace the replica dropdown, and windows of 3h or less don't poll offline events.

Not yet checked in Safari or Firefox.

Verification

image

Verified using teleport to customer environments
ResourceUsage.test.tsx renders the card against MSW handlers and covers:

  • each replica's CPU, memory and swap readings at a hovered time;
  • heap alone on environments without the swap columns;
  • memory labelled as a share of the allocation when no sample has a heap limit;
  • the CPU axis extending past 100% with the allocation line, and staying at 100% otherwise;
  • the out-of-memory marker in the tooltip;
  • the limit labels with their byte sizes;
  • a status row for a replica offline the whole window, and the status rows in a window with no usage;
  • the count of objects a replica has left to hydrate;
  • hiding replicas with the toggles;
  • the creation marker label;
  • the empty window and the load error;
  • a week of a scheduled cluster's refreshes: one toggle and one status row for r1, a line per refresh with more than one point, and bars as wide as their own bucket;
  • a Processes button only for sizes that run more than one process, and a drawer naming the busiest process and the average across processes;
  • status rows in the same order as the replica toggles;
  • no fallback message while replica status is still loading.

processSpreadModel.test.ts covers each bucket's busiest process and the average of the processes that reported, leaving out a process whose fetch failed, and memory against the heap limit with the allocation fallback. The drawer test answers its SUBSCRIBE through a new mockSubscribeSocket helper, which sends a completed snapshot over the WebSocket.

ClusterMetrics.test.tsx covers the error shown when the clusters subscribe fails before its first snapshot.

ClusterDetail.test.tsx mocks the new Metrics tab.

🤖 Generated with Claude Code

@leedqin
leedqin added this pull request to stack #39509 October 2, 2026 23:17
@leedqin
leedqin force-pushed the cluster-page-resource-usage branch 2 times, most recently from 25ad3fe to 2feb8b3 Compare October 2, 2026 23:51
@leedqin
leedqin force-pushed the cluster-page-resource-usage branch 5 times, most recently from ea9e4b3 to 9a74524 Compare October 5, 2026 15:19
@leedqin
leedqin marked this pull request as ready for review October 5, 2026 15:28
@leedqin
leedqin requested a review from a team as a code owner October 5, 2026 15:28
@leedqin
leedqin requested a review from jdonelson October 5, 2026 15:28
@linear-code

linear-code Bot commented Oct 6, 2026

Copy link
Copy Markdown

CNS-174

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 9a74524 to b54dacb Compare October 6, 2026 22:20
@def-

def- commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Swap sizes get a "Disk" chart that plots swap

console/src/platform/clusters/ClusterPage/ResourceUsage/ResourceUsage.tsx:419

The disk chart and the tooltip's Disk row appear whenever diskPercent !== null, but on swap sizes (the M.1 sizes) disk_percent measures swap usage, not scratch disk. Any swap-size cluster that swapped during the window gets a "Disk: % of scratch disk in use (spilled state)" chart, and its tooltip shows the same swap bytes twice, once as "Swap" and once as "Disk". The PR says the opposite should happen: "Swap sizes run without a scratch disk and report none".

Details
  • Swap replicas get no --scratch-directory, so clusterd reports no disk_bytes. The orchestrator then stores swap_bytes in disk_bytes (src/orchestrator-kubernetes/src/lib.rs:1616, "the Console expects the swap size to be reported in disk_bytes").
  • For swap sizes, mz_cluster_replica_sizes.disk_bytes is the size's disk_limit, i.e. the swap budget. It is nonzero for M.1 sizes, since --heap-limit = memory_limit + disk_limit (src/controller/src/clusters.rs:824-833).
  • So the utilization views' disk_percent (src/catalog/src/builtin/mz_internal.rs:8131) works out to swap used over swap budget. It is non-null whenever swap is above 0, because toReplicaUtilizationGraphData only nulls exact zeros.
  • The classic page avoids this: it shows its Disk graph only when cluster.disk or replica.disk is set, and that flag is NOT swap_enabled AND disk_bytes != 0.
  • The test fixture gives swap rows maxDiskPercent: null (ResourceUsage.test.tsx:105). "leaves out the disk chart when no replica reports disk usage" therefore passes on data that swap sizes never produce.

Fix: decide whether to show the chart and the tooltip's Disk row from the size's disk flag, the same check as the classic page's clusterHasDisk. Don't decide it from whether a disk reading exists.

2. LOW -- A transient null heap limit relabels the memory chart until the page remounts

console/src/platform/clusters/ClusterPage/ResourceUsage/useResourceUsageData.ts:156

hasHeapLimit reads a null heap_limit in a replica's latest metrics row as "this environment has no heap limit". In cloud, that row is also null whenever a metrics fetch fails, for example while a replica restarts or sits in crash-loop back-off. If every current replica's latest row is null when the page loads, the memory chart switches to "% of memory allocation" and "memory limit" and drops the byte sizes. It stays that way: useReplicaHeapLimits stops polling once each replica has any row, and it uses staleTime: Infinity.

Details

The orchestrator records an all-null row when the metrics API call fails (src/orchestrator-kubernetes/src/lib.rs:1577, :1590). It leaves heap_limit null when the clusterd usage fetch fails (:1572). mz_cluster_replica_metrics returns the latest row per process. A single-replica cluster that was just OOM-killed is the likeliest trigger, which is exactly when someone opens this chart. The bars still show the share of the heap limit for samples that had one, and on 26.44+ the orange line keeps its "RAM limit" label, so the chart contradicts itself. Keep polling while a replica's limit is null, or derive the flag from the history rows' heapLimitPercent.

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from b54dacb to 82652de Compare October 7, 2026 02:24
@leedqin leedqin mentioned this pull request Oct 7, 2026
const timelinesById = new Map<string, ReplicaTimeline>();
if (!history.data) return timelinesById;
const { startDate, endDate } = history.data;
for (const { id, data } of history.data.graphData) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Timelines are only built for replicas in graphData, i.e. ones that reported metrics in the window. A replica that's been offline the whole window has no samples, so it gets a toggle but no status row, though an offline stretch is exactly what the strip is for. If every replica is down, the card shows "No resource usage in this time period" and hides the strip entirely, which reads as idle, not broken. Status history already covers the current replicas, and transformReplicaTimeline already carries a pre-window offline transition across the window. Could the rows come from replicasById, and the empty state keep the status strip?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Rows now come from every replica, so one offline the whole window gets an offline row from status history, and a window with no usage keeps the status rows under the empty message. Both have tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Follow-up: my premise here was incomplete. In cloud a down replica still writes an all-null metrics row every minute (get_metrics falls back to ServiceProcessMetrics::default() when the metrics API call fails). So when every replica is down for the whole window, the card takes the hasNoUtilizationMetrics branch. It shows the metrics-collection alert and no Replica Status. That's the QA bot's Oct 8 finding.

A missing metrics-server produces the same all-null rows, so the readings alone can't tell the two apart. Status history can, and it's already loaded. Proposal, for when every reading in the window is empty:

  • Some replica was online in the window: something was running and wasn't measured. "Your replicas were running, but no resource usage was recorded for this period." Then the deployment-specific advice below.
  • Every replica was offline the whole window, and the last offline reason was oom-killed: "All replicas were offline for this period. They last went offline after running out of memory. See Troubleshoot memory usage."
  • Every replica was offline the whole window, with no reason recorded: "All replicas were offline for this period, and Materialize didn't record why. If they keep restarting, see Troubleshoot hydration failures."
  • Status unknown (history failed to load, or has no rows): "No resource usage was recorded for this period. The replicas may have been offline, or metrics may be unavailable." Then the deployment-specific advice below.

Deployment-specific advice, by appConfig.mode:

  • Cloud: "If this continues, contact support." Customers can't install or fix metrics-server in cloud, so the current advice isn't actionable there.
  • Self-managed: "Check that metrics-server is installed in your Kubernetes cluster."

In every case, show the Replica Status section below the message.

The memory and hydration guides each need a one-line entry in mz-doc-urls.json, as #36479 did. Support is already there. I'd avoid "increase the cluster size" for OOMs, since the memory guide lists several causes where a resize isn't the fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the follow-up. Implemented your proposal in 2488ee4: when no sample in the window has a reading, the card picks the message from replica status (ran unmeasured, all offline after an OOM, all offline with no reason, or unknown), adds the cloud or self-managed advice, and always shows Replica Status below. The two guides are in mz-doc-urls.json.

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch 2 times, most recently from d4e204d to de5b9a5 Compare October 8, 2026 04:11
@leedqin

leedqin commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. For 1, the Disk chart and the tooltip's Disk row are gone; the new page doesn't chart disk. For 2, the memory label now comes from the utilization rows: a window is labelled as a share of the memory allocation only when none of its samples has a heap limit. The heap limits query also keeps polling while a replica's latest limit is null, so the legend's byte sizes recover after a failed fetch.

@def-

def- commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- A cluster whose replicas are all down still loses its status strip

console/src/platform/clusters/ClusterPage/ResourceUsage/ResourceUsage.tsx:453

The new status rows assume an offline replica reports no samples, but in cloud a provisioned replica that is down still writes an all-null metrics row every minute. So when every replica has been down for the whole window, for example a single-replica cluster stuck Pending or crashing at boot, graphData is non-empty, hasNoUtilizationMetrics is true, and the card shows "Cluster metrics are unavailable ... ensure that pod metrics collection is enabled". The status strip, which would show the offline stretch, is never rendered. That is the all-down case the review thread asked to fix, and the alert points at metrics-server rather than the outage.

Details

Why the rows exist: the per-replica metrics task runs every 60s for as long as the replica is provisioned and sends whatever the orchestrator returns (src/controller/src/clusters.rs:963-967). When the pod has no running container, get_metrics returns ServiceProcessMetrics::default() (src/orchestrator-kubernetes/src/lib.rs:1577, :1590), and record_replica_metrics writes it as a row of nulls (src/controller/src/lib.rs:616-646). The _3h, _24h and _overview views keep those samples, giving null cpu_percent/memory_percent/disk_percent. Meanwhile a not-ready pod is recorded as Offline in status history (src/orchestrator-kubernetes/src/lib.rs:1386-1390), so the strip has the data it needs.

The new hasUsage === false branch only handles a window with no rows at all. The test fixture produces exactly that case: r2OfflineAllWindow drops r2's utilization rows (ResourceUsage.test.tsx:162, :179). Both new tests pass without covering what an offline replica actually reports. In multi-replica clusters the offline replica already had null samples and therefore a row, so the new no-samples path mostly covers the first minute after a replica is created.

To fix this, keep the status strip in the hasNoUtilizationMetrics branch, with the alert above it, or skip the alert when status history shows the replicas offline. A fixture that gives the offline replica null-valued rows would cover the real case.

leedqin added a commit that referenced this pull request Oct 8, 2026
### Motivation

Part of moving the Console to React 19. visx 3.12 caps React at 18 in
its peer range. Main also mixes `@visx/grid` and `@visx/shape` 4.0 with
the other visx packages on 3.12, which installs two copies of the shared
visx internals.

Supersedes #39496.

### Description

- All `@visx/*` packages move to 4.0.0, deduped to one copy each.
- visx 4 adds an `exports` map, so deep imports such as
`@visx/tooltip/lib/hooks/useTooltipInPortal` and
`@visx/responsive/lib/components/ParentSize` no longer resolve. They now
import from the package roots. This includes the `vi.mock` in
`ThresholdLineGraph.test.tsx`.
- visx 4's `ParentSize` puts its children in an absolutely positioned
box inside a `height: 100%` container, so it collapses to 0px tall when
its parent has no fixed height. Two charts relied on the old behavior:
- **Cluster overview utilization graphs:** they set their own height, so
they now measure only their width with `useParentSize`.
- **Source and sink error graph:** its `ParentSize` gets the graph's
fixed height.
  
The other five `ParentSize` call sites are already in fixed-height
boxes. The redesigned cluster page's resource usage chart (#39508) has
the same width-only `ParentSize`, so whichever PR lands second needs
this change there.

### Verification

Existing tests. jsdom doesn't do layout, so the tests can't catch the
collapse. In headless Chromium, the utilization graph renders 0px tall
on visx 4 without this change and at full height with it.

Worth eyeballing on the preview:
- the cluster overview CPU, memory and disk graphs;
- a source's and a sink's error graph;
- the freshness graph;
- the billing spend chart.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@def-

def- commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. HIGH -- On visx 4 the resource usage card renders only its header

console/src/platform/clusters/ClusterPage/ResourceUsage/ResourceUsage.tsx:464

The base now includes visx 4, whose ParentSize renders its children in an absolute; inset: 0; overflow: hidden box inside a height: 100% wrapper. Here that wrapper sits in the card's auto-height VStack, so it is 0px tall. The CPU and memory charts, the status strip and the "No resource usage" message are all clipped, and the Metrics tab shows an empty card with only the title, replica toggles and time picker.

Details

The style={{ width: "100%", minWidth: 0 }} prop doesn't help. visx 4 merges it over {width: 100%, height: 100%, position: relative}, while visx 3 replaced the defaults with it, so the chart used to sit in normal flow.

I rendered the real component tree in headless Chromium, with Chakra styles and visx 4's real ParentSize reporting a width. The wrapper measures 0px, the card 98px (header only), and 616px of chart content is clipped. elementFromPoint over the memory chart hits <html>. Making the wrapper a plain block on the same page restores the card to 714px, and the chart becomes hit-testable.

The tests mock ParentSize to a fixed 900×240 and jsdom does no layout, so every ResourceUsage test still passes. #39611 notes that whichever of the two PRs lands second needs this change here.

Fix: measure only the width, as #39611 did for UtilizationGraph:

-      <ParentSize debounceTime={10} style={{ width: "100%", minWidth: 0 }}>
-        {({ width }) =>
-          width > 0 && (
-            <ResourceUsageCharts
+      <Box ref={parentRef} width="100%" minWidth={0}>
+        {width > 0 && (
+          <ResourceUsageCharts

Call const { parentRef, width } = useParentSize({ debounceTime: 10 }); from @visx/responsive at the top of ResourceUsage, not inside the else branch that builds body.

@jdonelson jdonelson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On the memory graph, I would add a space or a tick mark or some way to indicate that each pair represents one point in time. As it is, it's not clear that each pair represents one single point in time.

At one point I think you had overlapping bars? I liked that as a solution.

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 8d2ede6 to 7ea4cc0 Compare October 15, 2026 15:11
@leedqin
leedqin force-pushed the cluster-page-resource-usage branch 2 times, most recently from e81d5d4 to 5e350cc Compare October 15, 2026 15:59
@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 5e350cc to 6bc0626 Compare October 15, 2026 16:09
@leedqin

leedqin commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, confirmed and fixed in 6bc0626: the card now measures only its width with useParentSize, as UtilizationGraph does since #39611.

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 6bc0626 to 2a22188 Compare October 15, 2026 17:50
@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 2a22188 to 2488ee4 Compare October 15, 2026 18:11
@leedqin

leedqin commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. The all-down case is fixed in 2488ee4 along the lines of the review thread: empty readings no longer hide Replica Status. The memory chart now overlaps each time's replica bars by half a bar, with a surface ring between them, so each group reads as one point in time.

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 2488ee4 to 7a0f846 Compare October 15, 2026 21:37
@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 7a0f846 to 69e9986 Compare October 15, 2026 21:55

@jdonelson jdonelson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the current bar chart is struggling to display processes per replica. It worked well with 2 or 3 plots (1 per replica) but for hefty cluster with 2 replicas and 8 processes each it starts to lose visual fidelity. The colors and line styles will cycle and different processes will have the same color/style. And as the bars increase in number, we won't be able to fit them all in the bounds and the time window will have to shrink.

rendered example:

Image

Some possible alternatives could be:

  • Display only the max process size per replica.
  • Display the max, min, and average using bounded range or candlestick graphs. Or the average could be a line while the max is shown via bars.
  • Drill-downs or toggles so that we're not showing everything at once.

Here are illustrations of these ideas along with some others:
https://data.europa.eu/apps/data-visualisation-guide/differences-in-time-series

https://observablehq.com/@observablehq/whats-different-timeseries-forecast-performance?collection=@observablehq/analyzing-time-series-data
("Perceiving the vertical distance or the shortest path?" section)

Image Image Image Image

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch 2 times, most recently from 203c05e to 97778e6 Compare October 16, 2026 17:55
@def-

def- commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Clusters with SCHEDULE = ON REFRESH get blank CPU and memory charts on wide windows

console/src/platform/clusters/ClusterPage/ResourceUsage/ResourceUsage.tsx:367

The merge width (replicaCount: series.length) and the memory bar slots (ResourceUsageCharts.tsx:654) count every replica charted in the window, including past replicas that never ran at the same time. A scheduled cluster gets a new replica id on every refresh, so a 7-day window holds dozens of series that never overlap. Buckets then merge until each replica is a single point, so no CPU line is drawn and memory bars shrink below 1px. The status strip and the replica toggles also grow by one "r1" row per refresh.

Details

Each refresh turn-on allocates a fresh replica id (src/adapter/src/coord/cluster_controller.rs:737, reason OnRefresh at :1030). The utilization views keep dropped replicas through mz_cluster_replica_history, so graphData has one series per refresh, all named r1.

Probe: I rendered ResourceUsage at this head with a 7-day window, replication factor 0, and 28 dropped replicas (one every 6h, 3 hourly buckets each). 0 of the 28 CPU paths had more than one point. Every memory bar was 0.88px wide. There were 28 toggles and 28 status rows (672px). With an hourly refresh (160 replicas), bars were 0.036px wide and the status strip was 3840px tall.

Positive control: on the same fixture, with replicaCount: 1 and bars not divided by the series count, all 28 CPU lines draw and bars are 2.9 to 7.9px wide.

The 24h window has the same problem. An hourly refresh gives 24 series over 5-minute source buckets, so mergedBucketMs picks 8h groups, and each replica, up for about 10 minutes, merges into one point.

To fix this, size the merge from the largest number of series that have data in the same source bucket, and slot each time's bars only among the series present at that time. Dropped replicas that share a name could also share one status row and one toggle.

@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 97778e6 to 0bb33c9 Compare October 16, 2026 19:41
@leedqin

leedqin commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, thanks. The card charts one series per replica again, and per-process detail moves to the workers drawer in #39889.

@leedqin

leedqin commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, fixed in 0bb33c9: bucket merging and bar slots count only the replicas reporting at the same time, and replicas that share a name share one toggle, color and status row.

Third in the cluster details redesign stack. The Metrics tab gets a
resource usage card: CPU, memory and replica status share one time axis
and one hover tooltip, with a toggle per replica.

- CPU: a line per replica, as a share of the replica's CPU allocation.
  Readings past 100% extend the axis, with a dashed line marking the
  allocation.
- Memory: a bar per replica as a share of the heap limit (RAM plus swap),
  on a fixed 0 to 100% axis so swap-heavy sizes don't stretch it. On 26.44
  and later the bar splits into RAM and swap, and the legend gives the RAM
  and heap limits in bytes. Earlier environments show heap alone. Where no
  sample has a heap limit, the labels say the bars are a share of the
  memory allocation.
- Replica status: a row per replica, including one offline the whole
  window. Running, finished hydration and offline stretches, a marker for
  each out-of-memory kill, and a badge with the count of objects still
  hydrating. A window with no usage still shows the status rows.
- Dotted markers where an index or materialized view was created on the
  cluster.
- Replicas that share a name, such as a scheduled cluster's refreshes, share
  one toggle, color and status row. A time's memory bars split only among
  the replicas reporting then.
- Processes: when a replica's size runs more than one process, a Processes
  button on the CPU chart opens a drawer with a tab per such replica. It
  compares the busiest process with the average one for CPU and memory, as
  shares of one process's allocation, over the card's window up to 24h. It
  reads each process's samples from a SUBSCRIBE that is open only while the
  drawer is.

Wide windows merge neighboring buckets, keeping each metric's peak, so
bars stay at least 10px wide for as many replicas as report at once. The time axis matches the classic graphs.
The mark layers are memoized, so hovering only redraws the cursor and
tooltip.
@leedqin
leedqin force-pushed the cluster-page-resource-usage branch from 0bb33c9 to e0e0670 Compare October 16, 2026 20:35
@leedqin

leedqin commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my last note: per-process detail now lives in a Processes drawer on this card, opened from the CPU chart for multi-process sizes. It compares each replica's busiest process with the average one, in e0e0670.

@def-

def- commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- The tooltip lists every same-name replica at every hovered time

console/src/platform/clusters/ClusterPage/ResourceUsage/ResourceUsage.tsx:106

tooltipSectionsAt gives each series the state from its group's merged timeline, so a replica with no reading at the hovered time still gets a section. On a scheduled cluster, hovering inside any refresh lists one "r1 · Running" header per refresh in the window, and only one of them has readings. A week of hourly refreshes gives about 160 headers.

Details

On the PR's own scheduled-cluster fixture (27 refreshes, 7-day window), hovering inside refresh 10 produced a tooltip with 27 "r1 100cc Running" headers and one set of CPU/Memory/Swap rows. As a positive control, skipping members that have no point and aren't the group's representative brought it down to 2 headers.

Before this commit, each dropped replica's timeline covered only its own lifetime, so stateAt was undefined outside it and the section was skipped. A fix is one section per group at a time: take the member with a point, and fall back to the group's state only when no member has one.

2. MEDIUM -- A past blue-green deployment's replicas merge with the current ones and inherit their OOMs

console/src/platform/clusters/ClusterPage/ResourceUsage/resourceUsageModel.ts:320

groupReplicasByName assumes same-name replicas ran one after another because a cluster allows one live replica per name. But graphData spans the cluster's blue-green lineage (useClusterLineageIds), and the past deployment's r1 ran alongside the current r1 while the new deployment hydrated. Both get one toggle, one color and one status row. An OOM of the new replica is then shown on the old one too.

Details

Probe: I gave the current r1 (cluster u9) a lineage row for a past r1 (cluster u8), overlapping for 10 hours, and OOM-killed the new one inside the overlap. The card showed a single "r1 100cc" toggle and two CPU lines with the same stroke. At the OOM, both tooltip sections read "r1 100cc Out of Memory", including the past replica at 20% CPU that was never killed. mergeTimelines lets the later-starting segment win, so from the new replica's creation on, the row shows only the new deployment's states.

DataPoint carries no cluster id, so the grouping can't tell the two apart. Keying groups on the cluster as well as the name would keep a past deployment's replicas separate.

3. MEDIUM -- The scheduled-cluster test fails one hour in three

console/src/platform/clusters/ClusterPage/ResourceUsage/ResourceUsage.test.tsx:764

"charts a scheduled cluster's refreshes as one replica" depends on the wall clock. It fails whenever the window's first bucket falls on a UTC hour divisible by 3, which is 8 hours of every day, so it will flake on main and on unrelated console PRs.

Details

At the mocked 900px width the plot is 828px, which gives 4.9px per hour. With one bar at a time, mergedBucketMs therefore picks 3h groups, aligned to epoch multiples of 3h. Each refresh's three hourly buckets start at FIRST_BUCKET_MS + 6k hours. When that hour is a multiple of 3, all three buckets merge into one group, so every CPU path is a single M…Z point and lines is empty.

I ran it in a clean copy at 20:34Z and it failed, with all 27 paths single points. Shifting NOW_MS by 0 to 5 hours failed at 0h and 3h and passed at the other shifts.

Pinning the clock (vi.setSystemTime) makes the result deterministic. At the failing alignment, though, the chart really draws no CPU line for any refresh, so the assertion describes something the code doesn't guarantee for every window start.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants