Repository source: docs/plans/production_slow_actions_slice1_instrumentation_plan_v1.md
Production Slow Actions Slice 1 Instrumentation Plan V1
This plan breaks out Slice 1 from production_slow_actions_client_responsiveness_plan_v1.md. The goal is to measure the full user-perceived path before changing behavior, so the next optimization work targets the actual production bottlenecks.
Goal
Add enough server and browser timing to answer, for every slow reported action:
did the browser spend time preparing identity, normalizing input, or rendering feedback before the request?
did the request wait on the global write lock?
did server time go to validation, canonical reads, git, read-model work, artifact invalidation, external providers, or response rendering?
did the user experience slow redirects or post-response reconciliation?
This slice should not introduce optimistic UI behavior yet. It should only add observability with minimal behavior risk.
Non-Goals
Do not change write semantics, lock timeout behavior, git commit behavior, read-model update behavior, or static artifact invalidation.
Do not add a new production telemetry backend in this slice.
Do not expose privileged analysis details or raw request contents in timing/debug output.
Do not block later optimistic UI work on perfect instrumentation coverage for every endpoint.
Current State
LocalWriteServicealready returnstimingsfor common write phases such aswrite_file,git_add,git_commit,git_rev_parse,read_model_incremental_*, fallback rebuild phases,artifact_invalidate, andtotal.Application::serverTimingHeaders()already emits valid timing names as aServer-Timingheader.Application::handleAnalyzePost()already records a more detailed non-write timing chain.ExecutionLock::withExclusiveLockTimed()exposeslock_wait, andLocalWriteServicemerges it into successful canonical write timings.Write endpoints now merge application-level pre-writer timings with write-service timings. Handler-level
totalcovers the full request handler, while writer-internaltotalis preserved aswrite_total.Provider-backed post analysis now returns an
external_providertiming bucket, and the Dedalus agent reply generator attaches the same bucket when used directly.Browser-side action duration now has passive
performance.mark()coverage for reaction clicks and compose-submit preparation, plus opt-in console summaries for debugging.
Timing Names
Use lowercase snake case so existing Server-Timing filtering accepts each metric.
Server timing names:
request_dataviewer_profileidentity_inspectcanonical_readlock_waitwrite_filegit_addgit_commitgit_rev_parseread_model_incremental_updateread_model_incremental_*read_model_*_fallbackartifact_invalidateexternal_providerresponse_renderwrite_totaltotal
Browser mark/measure names:
forum_action_startforum_identity_startforum_identity_readyforum_first_feedbackforum_fetch_startforum_response_receivedforum_reconcile_completeforum_action_complete
When a browser action has a specific type, include it in the detail object or measure label, for example apply_thread_tag, apply_post_tag, compose_reply, or compose_thread.
Implementation Slices
Slice 1A: Lock Wait Timing
Status:
implemented
ExecutionLock::withExclusiveLock()remains as a compatibility wrapperExecutionLock::withExclusiveLockTimed()returns the callback result andlock_waitLocalWriteServicewrite methods mergelock_waitinto returned timing arrays
Goal:
expose time spent waiting to acquire the global lock without changing lock behavior
Work:
add a low-risk way for
ExecutionLockto report wait durationpreferred shape: add
withExclusiveLockTimed(callable $callback): arrayreturning both callback result andlock_waitkeep existing
withExclusiveLock()as a compatibility wrapperupdate
LocalWriteServicewrite methods to mergelock_waitinto returnedtimingspreserve timeout behavior and error messages
Important detail:
lock_waitshould measure from the first lock attempt until successful acquisition, not callback execution time.If the lock is acquired immediately, report
0.0or the measured near-zero duration.
Candidate files:
src/ForumRewrite/Support/ExecutionLock.phpsrc/ForumRewrite/Write/LocalWriteService.phptests/LocalAppSmokeTest.phpor a focused lock test
Verification:
uncontended writes include
lock_waita contended lock test proves
lock_waitis greater than zeroexisting lock timeout tests continue passing
Slice 1B: Normalize Write Endpoint Timing Coverage
Status:
implemented
write-like API handlers now include
request_data,viewer_profile,target_profile, writer timings,write_total, and full handlertotalwhere applicableaccount key and approval redirects now include
Server-Timingexpected pre-writer validation failures now return timing headers where practical
Goal:
make timing headers available for every slow-capable write endpoint and redirect
Work:
ensure these success paths return
Server-Timing:POST /compose/threadPOST /compose/replyPOST /api/create_threadPOST /api/create_replyPOST /api/apply_thread_tagPOST /api/apply_post_tagPOST /account/keyPOST /api/link_identityPOST /profiles/{slug}/approvePOST /api/approve_userPOST /api/analyze_postPOST /api/generate_agent_reply
add missing pre-writer phase timings where they are materially outside
LocalWriteService:request_dataviewer_profiletarget profile lookup for approval
post lookup and generation status checks for agent reply
include timing headers on expected validation/error responses where feasible, especially identity-missing and validation failures before the writer is invoked
keep response bodies unchanged unless tests require a minimal update
Candidate files:
src/ForumRewrite/Application.phptests/ApplicationServerTimingTest.phptests/LocalAppSmokeTest.php
Verification:
existing server timing formatting tests pass
new tests assert timing headers on representative API success responses
approval and account key redirects include timing headers when successful
validation errors that occur after timing starts include
Server-Timing
Slice 1C: External Provider Timing
Status:
implemented
PostAnalysisServicemeasures analyzer execution and returnsexternal_provideron success and provider failureDedalusPostAnalyzermeasures the actual provider HTTP call and returns that value asexternal_providerDedalusAgentReplyGeneratoralso returnsexternal_provider; the current/api/generate_agent_replypath still primarily reports the wholeagent_replyflow because it usually reuses analysis-derived response text
Goal:
distinguish model-provider latency from local analysis/generation work
Work:
wrap Dedalus post-analysis provider calls with an
external_providertimingwrap agent reply generation provider calls with
external_provideroragent_external_providerif both can appear in one responseavoid leaking provider request payloads, response text, or privileged analysis details
preserve existing cached-analysis behavior and timing names
Candidate files:
src/ForumRewrite/Analysis/PostAnalysisService.phpsrc/ForumRewrite/Analysis/DedalusPostAnalyzer.phpsrc/ForumRewrite/Agent/DedalusAgentReplyGenerator.phpsrc/ForumRewrite/Application.php
Verification:
cached analysis can still report fast local timings
provider-backed analysis/generation reports an external-provider bucket
tests can use stub providers or reflection-level assertions without real network calls
Slice 1D: Browser Action Performance Marks
Status:
implemented
reaction clicks mark action start, identity preparation, first feedback, fetch start, response receipt, reconciliation, and completion
compose submits mark action start, identity preparation, first feedback, validation failures, and submit/error completion
the timing helpers no-op when browser performance APIs are unavailable
Goal:
measure user-perceived duration around frequent interactions without changing UI behavior
Work:
add a small browser timing helper in an existing asset or a new
public/assets/action_timing.jsuse
performance.mark()andperformance.measure()when availableadd marks to reaction actions first:
click/action start
identity preparation start
identity ready
first feedback paint path
fetch start
response received
reconciliation complete
action complete/error
add marks to compose submit preparation where current JS already owns normalization and draft handling
keep this passive:
no beacon submission by default
optionally print compact summaries only when a debug flag is present, for example
?debug_timing=1orlocalStorage.forum_debug_timing=1
Candidate files:
public/assets/thread_reactions.jspublic/assets/browser_signing.jstemplates/layout.phpif a new shared asset is addedtests/BrowserSigningNormalizationTest.phpif existing JS harnesses need stubs
Verification:
existing JS tests still pass
timing helper no-ops cleanly when
performanceis unavailabledebug output contains action type and durations, not form bodies, post bodies, public keys, private keys, or analysis content
Slice 1E: Slow Sample Debug Format
Status:
implemented
reaction fetches parse
Server-Timingresponse headers into debug summariesreaction and compose timing helpers print compact
[forum timing]console events only when?debug_timing=1orlocalStorage.forum_debug_timing=1is enableddebug summaries include action, status, duration buckets, parsed server timing, and error kind without request bodies, post bodies, keys, or analysis content
Goal:
make local and production debugging consistent without adding infrastructure
Work:
define a compact event shape for browser debug output:
actionstatusduration_msidentity_msnetwork_msserver_timingerror_kindwhen applicable
parse the
Server-Timingresponse header in browser timing helper when availableprint only when debug timing is explicitly enabled
document how to enable and collect a slow sample
Candidate files:
public/assets/thread_reactions.jspublic/assets/browser_signing.jsthis plan or a short runbook note if the workflow needs more detail
Verification:
a manual slow sample includes both browser duration and parsed server buckets
debug output does not include sensitive authored content
Recommended Implementation Order
Implement
lock_waittiming inExecutionLockandLocalWriteService.Extend
Server-Timingcoverage for approval and identity-linking success paths.Add pre-writer timings for reaction viewer-profile lookup and approval profile lookup.
Add browser timing helper and reaction marks.
Add compose-submit marks only where current JS already intercepts or prepares the submit.
Add external-provider timing if it can be done without broad provider interface churn; otherwise record it as the first follow-up from this slice.
Acceptance Criteria
This slice is complete when:
representative production write actions include
lock_waitinServer-Timingslow-capable write endpoints consistently expose timing on success
expected pre-writer failures include timing when practical
reaction clicks can produce a browser-side debug sample showing identity, network, server, and reconciliation duration
instrumentation is passive by default and does not submit telemetry externally
tests cover timing header formatting, lock wait reporting, and at least one newly instrumented endpoint
Risks
Adding timing return shapes around locks could accidentally alter exception propagation. Keep
withExclusiveLock()unchanged for existing callers and add a timed variant.Too many
Server-Timingfields can become noisy. Keep names stable and use the existing formatter filtering.Browser debug output can leak sensitive data if event payloads include request bodies. Only log durations, action names, status, and parsed timing buckets.
Provider timing may require small interface changes. If that becomes invasive, defer provider-specific timing rather than expanding this slice.
Follow-Up After Slice 1
Use collected data to choose the next optimization:
high
lock_wait: reduce global lock hold time or queue writes differentlyhigh browser identity time: cache or pre-warm identity preparation
high git time: inspect repository size, hooks, filesystem, and commit strategy
high read-model fallback time: fix incremental invalidation or stale-marker causes
high redirect/render time: move compose/reaction flows to fetch and optimistic rendering
high provider time: turn analysis and agent replies into clearer background jobs