feat: add developer-mode startup and stack hydration timing (#1619)

* feat: add developer-mode startup and stack hydration timing

Instrument boot-to-list and detail hydration with commit-aligned milestones, truthful request stages, and destination/gateway debug duration logs so performance work is guided by measurements.

* fix: redact stack names and complete hydration request stages

Stop logging stack identifiers in containers debug timing, and record state_dispatch (plus detail fetch spans) so copied reports match the advertised stage breakdown.
This commit is contained in:
Anso
2026-07-14 17:24:25 -04:00
committed by GitHub
parent 4079cb9198
commit b70a529656
26 changed files with 2449 additions and 32 deletions
+8
View File
@@ -1,4 +1,5 @@
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
import { markMilestone } from '@/lib/hydrationTiming';
type AppStatus = 'loading' | 'needsSetup' | 'notAuthenticated' | 'mfaChallenge' | 'authenticated';
@@ -109,6 +110,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
};
// One-shot boot milestone: the auth gate has resolved to a terminal status
// (setup, login, MFA, or authenticated), so the app can leave the splash.
useEffect(() => {
if (appStatus === 'loading') return;
markMilestone('auth_resolved');
}, [appStatus]);
useEffect(() => {
checkAuth();
const handleUnauthorized = () => {
+20
View File
@@ -1,5 +1,6 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { apiFetch } from '@/lib/api';
import { beginNodeSession, markMilestone } from '@/lib/hydrationTiming';
import type { Capability } from '@/lib/capabilities';
export type NodeMode = 'proxy' | 'pilot_agent';
@@ -56,6 +57,18 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
const nodeMetaRef = useRef<Map<number, NodeMeta>>(nodeMeta);
nodeMetaRef.current = nodeMeta;
// Open a fresh hydration-timing session the moment the active node id changes,
// during render (guarded by a ref to fire once per id). This must precede the
// shell's stack-list refresh so its attempt binds to the new node session:
// child effects (EditorLayout's node-switch effect that calls refreshStacks)
// run before this provider's effects, so an effect here would begin the
// session too late and the list milestone would never commit.
const hydrationNodeRef = useRef<number | null>(null);
if (activeNode && hydrationNodeRef.current !== activeNode.id) {
hydrationNodeRef.current = activeNode.id;
beginNodeSession(activeNode.id);
}
const fetchNodeMeta = useCallback(async (nodeId: number, force = false) => {
const cached = nodeMetaRef.current.get(nodeId);
if (cached && !force) {
@@ -157,6 +170,13 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
return () => window.removeEventListener('node-not-found', handleNodeNotFound);
}, [refreshNodes]);
// One-shot boot milestone: nodes have finished loading and an active node is
// resolved. Deduped by the store, so a later re-run is a no-op.
useEffect(() => {
if (isLoading || !activeNode) return;
markMilestone('nodes_resolved');
}, [isLoading, activeNode]);
const activeNodeMeta = useMemo(() => {
if (!activeNode) return null;
return nodeMeta.get(activeNode.id) ?? null;