Files
Maestro/CLAUDE-PERFORMANCE.md
Pedram Amini 0cee135132 ## CHANGES
- Persist Claude session context-usage percentage, surviving resumes and restarts 🧠
- New IPC/API: `claude:updateSessionContextUsage` to store live context stats 🔌
- Fix resumed sessions falsely showing 100% context from lifetime tokens 🧯
- Usage stats now preserve cost only; tokens intentionally zeroed 📉
- Resume now always fetches session origins to restore context usage 🧭
- Reconstruct context percent on resume by synthesizing equivalent input tokens 🧮
- Add `{{AGENT_HISTORY_PATH}}` template variable for prompts and commands 🧾
- System prompt gains Task Recall guidance using the history JSON file 🗂️
- Input processing now resolves history file path during prompt substitution 🧬
- Add Electron DevTools trace-export workarounds in performance docs 🛠️
2026-01-14 17:07:45 -06:00

8.1 KiB
Raw Blame History

CLAUDE-PERFORMANCE.md

Performance best practices for the Maestro codebase. For the main guide, see CLAUDE.md.

React Component Optimization

Use React.memo for list item components:

// Components rendered in arrays (tabs, sessions, list items) should be memoized
const Tab = memo(function Tab({ tab, isActive, ... }: TabProps) {
  // Memoize computed values that depend on props
  const displayName = useMemo(() => getTabDisplayName(tab), [tab.name, tab.agentSessionId]);

  // Memoize style objects to prevent new references on every render
  const tabStyle = useMemo(() => ({
    borderRadius: '6px',
    backgroundColor: isActive ? theme.colors.accent : 'transparent',
  } as React.CSSProperties), [isActive, theme.colors.accent]);

  return <div style={tabStyle}>{displayName}</div>;
});

Consolidate chained useMemo calls:

// BAD: Multiple dependent useMemo calls create cascade re-computations
const filtered = useMemo(() => sessions.filter(...), [sessions]);
const sorted = useMemo(() => filtered.sort(...), [filtered]);
const grouped = useMemo(() => groupBy(sorted, ...), [sorted]);

// GOOD: Single useMemo with all transformations
const { filtered, sorted, grouped } = useMemo(() => {
  const filtered = sessions.filter(...);
  const sorted = filtered.sort(...);
  const grouped = groupBy(sorted, ...);
  return { filtered, sorted, grouped };
}, [sessions]);

Pre-compile regex patterns at module level:

// BAD: Regex compiled on every render
const Component = () => {
  const cleaned = text.replace(/^(\p{Emoji})+\s*/u, '');
};

// GOOD: Compile once at module load
const LEADING_EMOJI_REGEX = /^(\p{Emoji})+\s*/u;
const Component = () => {
  const cleaned = text.replace(LEADING_EMOJI_REGEX, '');
};

Memoize helper function results used in render body:

// BAD: O(n) lookup on every keystroke (runs on every render)
const activeTab = activeSession ? getActiveTab(activeSession) : undefined;
// Then used multiple times in JSX...

// GOOD: Memoize once, use everywhere
const activeTab = useMemo(
  () => activeSession ? getActiveTab(activeSession) : undefined,
  [activeSession?.aiTabs, activeSession?.activeTabId]
);
// Use activeTab directly in JSX - no repeated lookups

Data Structure Pre-computation

Build indices once, reuse in renders:

// BAD: O(n) tree traversal on every markdown render
const result = remarkFileLinks({ fileTree, cwd });

// GOOD: Build index once when fileTree changes, pass to renders
const indices = useMemo(() => buildFileTreeIndices(fileTree), [fileTree]);
const result = remarkFileLinks({ indices, cwd });

Main Process (Node.js)

Cache expensive lookups:

// BAD: Synchronous file check on every shell spawn
fs.accessSync(shellPath, fs.constants.X_OK);

// GOOD: Cache resolved paths
const shellPathCache = new Map<string, string>();
const cached = shellPathCache.get(shell);
if (cached) return cached;
// ... resolve and cache
shellPathCache.set(shell, resolved);

Use async file operations:

// BAD: Blocking the main process
fs.unlinkSync(tempFile);

// GOOD: Non-blocking cleanup
import * as fsPromises from 'fs/promises';
fsPromises.unlink(tempFile).catch(() => {});

Debouncing and Throttling

Use debouncing for user input and persistence:

// Session persistence uses 2-second debounce to prevent excessive disk I/O
// See: src/renderer/hooks/utils/useDebouncedPersistence.ts
const { persist, isPending } = useDebouncedPersistence(session, 2000);

// Always flush on visibility change and beforeunload to prevent data loss
useEffect(() => {
  const handleVisibilityChange = () => {
    if (document.hidden) flushPending();
  };
  document.addEventListener('visibilitychange', handleVisibilityChange);
  window.addEventListener('beforeunload', flushPending);
  return () => { /* cleanup */ };
}, []);

Debounce expensive search operations:

// BAD: Fuzzy matching all files on every keystroke
const suggestions = useMemo(() => {
  return getAtMentionSuggestions(atMentionFilter);  // Runs 2000+ fuzzy matches per keystroke
}, [atMentionFilter]);

// GOOD: Debounce the filter value first (100ms is imperceptible)
const debouncedFilter = useDebouncedValue(atMentionFilter, 100);
const suggestions = useMemo(() => {
  return getAtMentionSuggestions(debouncedFilter);  // Only runs after user stops typing
}, [debouncedFilter]);

Use throttling for high-frequency events:

// Scroll handlers should be throttled to ~4ms (240fps max)
const handleScroll = useThrottledCallback(() => {
  // expensive scroll logic
}, 4);

Update Batching

Batch rapid state updates during streaming:

// During AI streaming, IPC triggers 100+ updates/second
// Without batching: 100+ React re-renders/second
// With batching at 150ms: ~6 renders/second
// See: src/renderer/hooks/session/useBatchedSessionUpdates.ts

// Update types that get batched:
// - appendLog (accumulated via string chunks)
// - setStatus (last wins)
// - updateUsage (accumulated)
// - updateContextUsage (high water mark - never decreases)

Virtual Scrolling

Use virtual scrolling for large lists (100+ items):

// See: src/renderer/components/HistoryPanel.tsx
import { useVirtualizer } from '@tanstack/react-virtual';

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => scrollRef.current,
  estimateSize: () => 40,  // estimated row height
});

IPC Parallelization

Parallelize independent async operations:

// BAD: Sequential awaits (4 × 50ms = 200ms)
const branches = await git.branch(cwd);
const remotes = await git.remote(cwd);
const status = await git.status(cwd);

// GOOD: Parallel execution (max 50ms = 4x faster)
const [branches, remotes, status] = await Promise.all([
  git.branch(cwd),
  git.remote(cwd),
  git.status(cwd),
]);

Visibility-Aware Operations

Pause background operations when app is hidden:

// See: src/renderer/hooks/git/useGitStatusPolling.ts
const handleVisibilityChange = () => {
  if (document.hidden) {
    stopPolling();  // Save battery/CPU when backgrounded
  } else {
    startPolling();
  }
};
document.addEventListener('visibilitychange', handleVisibilityChange);

Context Provider Memoization

Always memoize context values:

// BAD: New object on every render triggers all consumers to re-render
return <Context.Provider value={{ sessions, updateSession }}>{children}</Context.Provider>;

// GOOD: Memoized value only changes when dependencies change
const contextValue = useMemo(() => ({
  sessions,
  updateSession,
}), [sessions, updateSession]);
return <Context.Provider value={contextValue}>{children}</Context.Provider>;

Event Listener Cleanup

Always clean up event listeners:

useEffect(() => {
  const handler = (e: Event) => { /* ... */ };
  document.addEventListener('click', handler);
  return () => document.removeEventListener('click', handler);
}, []);

Performance Profiling

Exporting DevTools Performance traces:

The Chrome DevTools Performance panel's "Save profile" button fails in Electron with:

NotAllowedError: The request is not allowed by the user agent or the platform in the current context.

This occurs because Electron 28 doesn't fully support the File System Access API (showSaveFilePicker). Full support was added in Electron 30+ (electron/electron#41419).

Workarounds:

  1. Launch with experimental flag (enables FSAA):

    # macOS
    /Applications/Maestro.app/Contents/MacOS/Maestro --enable-experimental-web-platform-features
    
    # Development
    npm run dev -- --enable-experimental-web-platform-features
    
  2. Use Maestro's native save dialog (copy trace JSON from DevTools, then in renderer console):

    navigator.clipboard.readText().then(data =>
      window.maestro.dialog.saveFile({ defaultPath: 'trace.json', content: data })
    );
    
  3. Right-click context menu - Right-click on the flame graph and select "Save profile..." which may use a different code path.