Skip to content

doc: design for reporting server time for SQL statements - #38443

Draft
tonydu-mz wants to merge 3 commits into
mainfrom
tonydu/cs-218-console-response-time-metric-should-show-actual-database
Draft

doc: design for reporting server time for SQL statements#38443
tonydu-mz wants to merge 3 commits into
mainfrom
tonydu/cs-218-console-response-time-metric-should-show-actual-database

Conversation

@tonydu-mz

@tonydu-mz tonydu-mz commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Motivation

The Console SQL Shell reports a single number, Returned in 148ms, dominated by network round trip and browser work. The metric is not wrong — it is exactly what its tooltip claims — but it asks for something no client can compute, because Materialize never tells any client how long it spent.

That gap is not Console-specific. A coding agent over MCP, a psql user, and a dbt run are all equally unable to separate their own latency from ours.

Design doc only. No behaviour change.

Part of CS-218.

Description

Adds doc/developer/design/20260824_console_server_time_metric.md, proposing that Materialize measure receipt-to-result-available per statement and report it as an opt-in structured notice, with the Console rendering 1.2ms server · 148ms total.

The doc has been rewritten after an independent review of the first revision, which found five blocking issues. Reviewers of the earlier version should read the current text rather than the diff.

Transport is a notice, not a new WebSocket message. The deciding argument is reach. A WebSocketResponse variant serves WebSocket clients and nothing else; a notice already reaches pgwire, WebSocket and the HTTP SQL API. Given the direction toward headless and agent-driven use, a Console-only mechanism was the wrong shape. It also has exact prior art in emit_plan_insights_notice, which already ships a structured JSON payload per statement under its own SQLSTATE and which the Console already dispatches on.

This adds a fourth work item: MCP currently discards notices (src/environmentd/src/http/mcp.rs:1061-1072). Fixing that is a prerequisite here and worth doing regardless, since an agent that cannot see notices also cannot see plan insights or deprecation warnings.

The measurement boundary is the substance of the review. Receipt is stamped per statement, because the only existing notion is per-request and statement N would otherwise include statements 1..N-1. Result-available is stamped before row serialization, because stamping before CommandComplete would count socket backpressure as server time and make the two numbers converge for large results. Writes are excluded, because the implicit commit runs after the result is retired, so measuring to result-available would report a sub-millisecond figure for a write whose durable work had not happened. Whether to ship writes anyway is an open question in the doc.

The clock is a monotonic Instant in microseconds. EpochMillis cannot express a sub-millisecond peek and can step backwards under NTP.

The doc also records why the obvious implementations do not work: the statement-lifecycle timestamps are behind sampling, and LifecycleTimestamps is pgwire-only and never populated on this path.

Includes an implementation plan across four work items in three CODEOWNERS scopes, with testing strategy, observability, frame cost, rollout and a risk register.

Verification

Documentation only. Line citations were re-verified against this PR's base commit rather than carried over from review.

Three open questions are flagged for reviewers to close before merge: whether writes ship in v1, whether Query Insights should follow the new metric, and confirming with a real environment that the server/round-trip split is actually large enough to be worth surfacing.

🤖 Generated with Claude Code

Design doc for CS-218. The Shell's "Returned in Xms" is a client-side
round trip: it spans WebSocket send, network, server work, network back,
JSON parse and FSM dispatch. That is what its tooltip already claims, so
this is not a bug but a request for a number the client cannot compute,
because the server never reports how long it spent.

Proposes that the server measure receipt to response-ready, report it via
a new opt-in WebSocketResponse variant, and that the Shell render both
numbers as "1.2ms server - 148ms total".

Three choices carry the design:

- The interval is receipt to response-ready, labelled "server time", not
  ExecutionBegan to ExecutionFinished. The gap between them is coordinator
  queueing, which was the dominant term in several 2026 incidents; a metric
  excluding it would show ~1ms while a user waited seconds.
- The clock is a monotonic Instant reported in microseconds. EpochMillis
  cannot express a sub-millisecond peek and can step backwards under NTP.
- The message is opt-in via the handshake's existing session-variable map,
  because the WebSocket API is public and its documented type table reads
  as exhaustive with no forward-compatibility clause.

Also records why the statement-lifecycle machinery cannot supply this: it
is gated on both sampling and a dyncfg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 25, 2026

Copy link
Copy Markdown

CS-218

Independent review found five blocking issues and two factual errors in
the first draft. Rewritten rather than patched.

Transport switched from a new WebSocketResponse variant to an opt-in
structured AdapterNotice, following emit_plan_insights_notice. The
deciding argument is reach: a WS variant serves WebSocket clients and
nothing else, while a notice already reaches pgwire, WebSocket and the
HTTP SQL API. Given the direction toward headless and agent use, a
Console-only mechanism was the wrong shape. Adds a fourth work item:
MCP currently discards notices (mcp.rs:1061-1072), which is a
prerequisite here and worth fixing regardless.

Corrections:
- The metric's start stamp is the previous statement's endTimeMs, not
  commandSentTimeMs, for every statement after the first.
- Receipt must be stamped per statement; the only existing notion is
  per-request, so statement N would have included statements 1..N-1.
- Result-available is stamped before row serialization, not before
  CommandComplete, which would have counted socket backpressure as
  server time.
- Writes are excluded: the implicit commit runs after the result is
  retired, so measuring to result-available would report a
  sub-millisecond figure for a write whose durable work had not
  happened.
- SUBSCRIBE does send CommandComplete, so streaming needs an explicit
  exclusion rather than relying on it never completing.
- LifecycleTimestamps is pgwire-only and unavailable on this path.
- The coordinator-queueing rationale was stale: fast-path peeks now
  bypass the coordinator main task by default. Argument rewritten to
  rest on stability of the boundary rather than on where time is spent.

Adds a wire shape, transaction semantics, an implementation plan across
four work items, testing strategy, observability, cost, rollout and a
risk register.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tonydu-mz tonydu-mz changed the title doc: design for reporting server time in the Console SQL Shell doc: design for reporting server time for SQL statements Aug 25, 2026
…ke points

Second review found the revision-2 proposal could not work: the emission
point was in environmentd, which src/pgwire does not depend on, so the
pgwire reach argument was false; and "before the row loop" is before the
rows exist, since SendingRowsStreaming wraps a receiver for a peek just
dispatched, so a slow query would have reported near-zero.

Rewritten around what the code already does rather than a new mechanism.

The measurement already exists. SessionClient::execute stamps
execute_started (client.rs:815) and returns it to both transports.
RecordFirstRowStream already holds both stamps, execute_started (:2146)
and recorded_first_row_instant (:2153), and recv observes the interval
into time_to_first_row_seconds (:2222-2229). The wrapper is constructed
on all nine transport sites. This design exposes a number Materialize
computes and discards, rather than inventing one.

Delivery cannot use the session notice queue, and this is structural:
pgwire flushes notices before send_execute_response (protocol.rs:
1152-1153), and the WS path appends them after CommandComplete
(sql.rs:1356-1371). A value that exists only after the first row is
therefore misattributed. emit_plan_insights_notice escapes this only
because it fires before execution. Instead each transport emits at a
point it already orders: pgwire's command_complete! macro, the WS msgs
vector, and SqlResponse::add_result. Three integration points, stated as
a cost rather than claimed as free.

Scope narrowed to row-returning statements, because time to first row is
undefined without rows. Records that this collides with the version-skew
fallback rendering, since an INSERT shows a duration today, and raises it
as a blocking open question rather than hiding it.

Prototype step is now measuring the actual split before building, since
the whole premise rests on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant