Skip to content

feat(controlplane): log client-correlated fork submit-to-ready latency#868

Open
stubbi wants to merge 7 commits into
mainfrom
perf/fork-latency-instrument
Open

feat(controlplane): log client-correlated fork submit-to-ready latency#868
stubbi wants to merge 7 commits into
mainfrom
perf/fork-latency-instrument

Conversation

@stubbi

@stubbi stubbi commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Mitos boots Firecracker microVMs and forks them via copy-on-write snapshots, exposed through CRDs and a hosted SDK.
  • Subsystem: the SaaS control plane fork path (internal/saas/controlplane/fork.go) plus the fork reconciler it hands off to.
  • Hosted co-located fork latency was being read off the controller "fork timing complete" totalMs, which measures forkStartedAt to the END of the reconcile pass and keeps counting after the ready watch already returned to the client.
  • Prod v1.38 measurements proved totalMs is decorrelated from client latency (a fork logged totalMs 791.8 ms while its server submit-to-Ready was 248 ms and client wall 437 ms), so latency work was aiming at a phantom "~200 ms scheduling gap" that is an artifact of the wrong metric. This serves the fork-performance line of ROADMAP.md.
  • This pull request logs an authoritative, client-correlated submit-to-ready split in the fork path and adds a design note capturing the measured breakdown and the synchronous fast-path evaluation.
  • The benefit is that server-side fork latency is now repeatable and honest, so the next fork-latency increment targets the real cost (network RTT + husk RPC work), not the overstated totalMs.

Linked Issues or Issue Description

No dedicated issue. Problem: there is no authoritative, client-correlated log of
hosted fork latency; the controller totalMs overstates it and is decorrelated
from what the client waits for, which misdirects fork-latency work. This PR adds
the missing measurement and documents where the latency actually lives.

What Changed

  • internal/saas/controlplane/fork.go: after the fork Sandbox is created, log a
    structured fork served line with submitToCreateMs (request receipt to a
    persisted CR) and submitToReadyMs (request receipt to the ready watch
    observing the child Ready). Observational only; the fork control flow is
    unchanged (same pollReady call, same response).
  • docs/perf/fork-fast-path.md: investigation note. Measured prod breakdown
    (5 forks, co-located, reconcilePasses=1), the finding that totalMs
    overstates, the finding that the p50 scheduling gap is ~20-35 ms (not ~200 ms),
    the tail-jitter verdict, and the synchronous fork fast-path design with its
    correctness constraints and estimated (modest p50, real p99) drop.

Verification

  • go build ./internal/saas/controlplane/ (exit 0)
  • go test ./internal/saas/controlplane/ (ok)
  • golangci-lint run ./internal/saas/controlplane/ (darwin, exit 0)
  • GOOS=linux golangci-lint run ./internal/saas/controlplane/ (exit 0)
  • Numbers in the doc are from prod v1.38 (co-location + husk conn reuse ON):
    client wall + server fork_time_ms via the SDK, per-stage + totalMs via the
    controller fork stage timing / fork timing complete logs.

Risks

Low risk. Adds one INFO log line per fork and a docs file. No control-flow,
schema, or response change. The log carries only the sandbox name and two
integer millisecond values (no secrets, no token).

Model Used

Claude Opus 4.8 (claude-opus-4-8), high reasoning effort.

Checklist

  • PR title is a conventional commit (feat)
  • Thinking Path traces from project context to this change
  • Model Used is filled in (with version and capability details)
  • Tests added for behavior changes, in the same commit (TDD) - N/A, no behavior change (logging + docs only); existing controlplane tests pass
  • Docs updated in the same PR
  • Threat-model delta included if the security surface moved - N/A, surface unchanged
  • Benchmark run included if the hot path was touched - N/A, no hot-path code change; measurements are in the doc
  • No em or en dashes introduced anywhere
  • Secret values never logged, in errors, in condition messages, or on host paths
  • No internal/instance-local references (only public #NNN / mitos-run/mitos URLs)
  • Every commit carries a Signed-off-by trailer (git commit -s)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added more detailed fork latency reporting, so fork requests now provide clearer timing insight for creation and readiness.
  • Documentation
    • Added a performance note describing hosted fork behavior, observed timings, and a proposed faster fork path for future improvement.

The controller "fork timing complete" totalMs measures forkStartedAt to the
end of the reconcile pass, which keeps counting after the ready watch already
returned to the client. Prod v1.38 measurements show it decorrelated from
client latency (one fork logged totalMs 791.8 while its server submit-to-Ready
was 248 and client wall 437), so it cannot be used as the client-latency proxy.

Log an authoritative, client-correlated split in the fork path instead:
submitToCreateMs (request receipt to a persisted fork CR) and submitToReadyMs
(request receipt to the ready watch observing the child Ready). The difference
is the controller scheduling + fork work + watch delivery, making the
server-side fork latency repeatable without depending on the misleading totalMs.

Purely observational: no change to the fork control flow. Adds a design note
(docs/perf/fork-fast-path.md) with the measured breakdown and the synchronous
fast-path evaluation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a performance investigation document analyzing hosted fork latency and proposing a synchronous fast-path design, and instruments the control-plane fork handler with structured logging that records submitToCreateMs and submitToReadyMs timing metrics.

Changes

Fork Latency Investigation and Telemetry

Layer / File(s) Summary
Fork handler timing instrumentation
internal/saas/controlplane/fork.go
Adds log/slog import and captures timestamps around create and ready phases, logging a fork served event with submitToCreateMs and submitToReadyMs before returning the unchanged poll result.
Fork fast-path performance design document
docs/perf/fork-fast-path.md
New document covering current hosted fork flow timing, measured warm/cold results, findings on misleading controller totalMs metrics, a proposed synchronous fast-path design with correctness constraints, expected latency impact, and ranked optimization levers tied to the new telemetry.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • mitos-run/mitos#710: Both PRs modify the hosted fork handler in internal/saas/controlplane/fork.go, one adding timing/metrics logging and the other introducing live fork handler semantics.
  • mitos-run/mitos#735: Both PRs modify the same fork handler codepath in internal/saas/controlplane/fork.go, one adding timing instrumentation and the other expanding request validation and secret inheritance wiring.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is a clear conventional commit that matches the main change: client-correlated fork latency logging.
Description check ✅ Passed The description includes all required sections and gives enough context, changes, verification, risks, and model details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/fork-latency-instrument

Comment @coderabbitai help to get the list of available commands.

@stubbi stubbi enabled auto-merge (squash) July 8, 2026 22:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/saas/controlplane/fork.go`:
- Around line 172-189: The "fork served" telemetry in fork() is logged
unconditionally even when pollReady() returns an error, which mixes failed waits
into the success latency bucket. Update the flow around resp, err :=
k.pollReady(...) so slog.Info("fork served", ...) only runs on success, or add
explicit success/error handling and a status field, using the existing symbols
pollReady, fork served, submitToCreateMs, and submitToReadyMs to keep the
client-correlated metrics accurate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7f360544-217d-4de9-a089-e35b2295a247

📥 Commits

Reviewing files that changed from the base of the PR and between 79b773f and 164eab8.

📒 Files selected for processing (2)
  • docs/perf/fork-fast-path.md
  • internal/saas/controlplane/fork.go

Comment on lines +172 to +189
// Timing split for the fork latency investigation. submitToCreateMs is the
// control plane's own cost to persist the fork Sandbox (request receipt to a
// successful Create); submitToReadyMs is the full server-side wall the client
// waits on (request receipt to the ready watch observing the child Ready), so
// the difference is the controller scheduling + fork work + watch delivery.
// This is the authoritative, client-correlated server-side fork latency: it
// does NOT depend on the controller's "fork timing complete" totalMs, which
// measures to the end of the reconcile pass and overstates the client-observed
// latency (it can keep counting after the ready watch already returned).
// Purely observational: it does not change the fork control flow.
createdAt := k.now()
resp, err := k.pollReady(ctx, ns, name, startedAt)
slog.Info("fork served",
"sandbox", name,
"submitToCreateMs", createdAt.Sub(startedAt).Milliseconds(),
"submitToReadyMs", k.now().Sub(startedAt).Milliseconds(),
)
return resp, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

"fork served" is logged even when pollReady fails.

resp, err := k.pollReady(...) can return a non-nil err (e.g. ready-wait timeout), but the slog.Info("fork served", ...) call fires unconditionally with the same submitToReadyMs value. Since this telemetry is explicitly meant to be the "authoritative, client-correlated" latency number the design doc relies on, mixing failed/timed-out polls into the same "served" bucket without any success/error marker will silently corrupt future p50/p99 latency analysis — the same class of problem this PR is fixing for totalMs.

🐛 Proposed fix to distinguish success/failure
 	createdAt := k.now()
 	resp, err := k.pollReady(ctx, ns, name, startedAt)
-	slog.Info("fork served",
+	slog.Info("fork served",
 		"sandbox", name,
 		"submitToCreateMs", createdAt.Sub(startedAt).Milliseconds(),
 		"submitToReadyMs", k.now().Sub(startedAt).Milliseconds(),
+		"success", err == nil,
 	)
 	return resp, err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Timing split for the fork latency investigation. submitToCreateMs is the
// control plane's own cost to persist the fork Sandbox (request receipt to a
// successful Create); submitToReadyMs is the full server-side wall the client
// waits on (request receipt to the ready watch observing the child Ready), so
// the difference is the controller scheduling + fork work + watch delivery.
// This is the authoritative, client-correlated server-side fork latency: it
// does NOT depend on the controller's "fork timing complete" totalMs, which
// measures to the end of the reconcile pass and overstates the client-observed
// latency (it can keep counting after the ready watch already returned).
// Purely observational: it does not change the fork control flow.
createdAt := k.now()
resp, err := k.pollReady(ctx, ns, name, startedAt)
slog.Info("fork served",
"sandbox", name,
"submitToCreateMs", createdAt.Sub(startedAt).Milliseconds(),
"submitToReadyMs", k.now().Sub(startedAt).Milliseconds(),
)
return resp, err
// Timing split for the fork latency investigation. submitToCreateMs is the
// control plane's own cost to persist the fork Sandbox (request receipt to a
// successful Create); submitToReadyMs is the full server-side wall the client
// waits on (request receipt to the ready watch observing the child Ready), so
// the difference is the controller scheduling + fork work + watch delivery.
// This is the authoritative, client-correlated server-side fork latency: it
// does NOT depend on the controller's "fork timing complete" totalMs, which
// measures to the end of the reconcile pass and overstates the client-observed
// latency (it can keep counting after the ready watch already returned).
// Purely observational: it does not change the fork control flow.
createdAt := k.now()
resp, err := k.pollReady(ctx, ns, name, startedAt)
slog.Info("fork served",
"sandbox", name,
"submitToCreateMs", createdAt.Sub(startedAt).Milliseconds(),
"submitToReadyMs", k.now().Sub(startedAt).Milliseconds(),
"success", err == nil,
)
return resp, err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/saas/controlplane/fork.go` around lines 172 - 189, The "fork served"
telemetry in fork() is logged unconditionally even when pollReady() returns an
error, which mixes failed waits into the success latency bucket. Update the flow
around resp, err := k.pollReady(...) so slog.Info("fork served", ...) only runs
on success, or add explicit success/error handling and a status field, using the
existing symbols pollReady, fork served, submitToCreateMs, and submitToReadyMs
to keep the client-correlated metrics accurate.

@stubbi

stubbi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed: log placement is after all Create error returns, uses the injected clock, carries no secrets, and the doc states its provenance (dated prod logs, method described) rather than claiming bench numbers. One named follow-up, not blocking: the fork served line also fires when pollReady returns a timeout/failure response with no outcome discriminator, so percentiles derived from it can be skewed by timed-out forks; add an outcome field in a follow-up. Merging via auto-merge once the branch is updated.

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