## CHANGES

- Added core goals emphasizing responsiveness, battery, and memory efficiency docs 🚀
- Introduced detailed performance guidelines for React rendering, timers, IPC 🧭
- Optimized worktree child sorting from O(n²) to O(n) using Map 
- Memoized per-parent worktree children lists to avoid repeated filtering 🧠
- Sorted worktree children once per parent, reducing redundant comparisons 🔧
- Added memoized `groupsById` Map for instant group lookups during filtering 🗺️
- Eliminated repeated `Array.find()` calls when computing visible sessions 🏎️
This commit is contained in:
Pedram Amini
2025-12-25 01:42:37 -06:00
parent 785480b7fc
commit 2a53dbc5fc
2 changed files with 109 additions and 11 deletions

View File

@@ -6,6 +6,16 @@ Thank you for your interest in contributing to Maestro! This document provides g
For architecture details, see [ARCHITECTURE.md](ARCHITECTURE.md). For quick reference while coding, see [CLAUDE.md](CLAUDE.md).
## Core Goals
**Snappy interface and reduced battery consumption are fundamental goals for Maestro.** Every contribution should consider:
- **Responsiveness**: UI interactions should feel instant. Avoid blocking the main thread.
- **Battery efficiency**: Minimize unnecessary timers, polling, and re-renders.
- **Memory efficiency**: Clean up event listeners, timers, and subscriptions properly.
See [Performance Guidelines](#performance-guidelines) for specific practices.
## Table of Contents
- [Development Setup](#development-setup)
@@ -16,6 +26,7 @@ For architecture details, see [ARCHITECTURE.md](ARCHITECTURE.md). For quick refe
- [Common Development Tasks](#common-development-tasks)
- [Adding a New AI Agent](#adding-a-new-ai-agent)
- [Code Style](#code-style)
- [Performance Guidelines](#performance-guidelines)
- [Debugging Guide](#debugging-guide)
- [Commit Messages](#commit-messages)
- [Pull Request Process](#pull-request-process)
@@ -497,6 +508,66 @@ For detailed implementation guide, see [AGENT_SUPPORT.md](AGENT_SUPPORT.md).
- Sanitize all user inputs
- Use `spawn()` with `shell: false`
## Performance Guidelines
Maestro prioritizes a snappy interface and minimal battery consumption. Follow these guidelines:
### React Rendering
- **Memoize expensive computations** with `useMemo` - especially sorting, filtering, and transformations
- **Use Maps for lookups** instead of `Array.find()` in loops (O(1) vs O(n))
- **Batch state updates** - use the `useBatchedSessionUpdates` hook for high-frequency IPC updates
- **Avoid creating objects/arrays in render** - move static objects outside components or memoize them
```typescript
// Bad: O(n) lookup in every iteration
sessions.filter(s => {
const group = groups.find(g => g.id === s.groupId); // O(n) per session
return group && !group.collapsed;
});
// Good: O(1) lookup with memoized Map
const groupsById = useMemo(() => new Map(groups.map(g => [g.id, g])), [groups]);
sessions.filter(s => {
const group = groupsById.get(s.groupId); // O(1)
return group && !group.collapsed;
});
```
### Timers & Intervals
- **Prefer longer intervals** - 3 seconds instead of 1 second for non-critical updates
- **Use `setTimeout` sparingly** - consider if the delay is truly necessary
- **Clean up all timers** in `useEffect` cleanup functions
- **Avoid polling** - use event-driven updates via IPC when possible
```typescript
// RightPanel.tsx uses 3-second intervals for elapsed time updates
intervalRef.current = setInterval(updateElapsed, 3000); // Not 1000ms
```
### Memory & Cleanup
- **Remove event listeners** in cleanup functions
- **Clear Maps and Sets** when no longer needed
- **Use WeakMap/WeakSet** for caches that should allow garbage collection
- **Limit log buffer sizes** - truncate old entries when buffers grow large
### IPC & Data Transfer
- **Batch IPC calls** - combine multiple small calls into fewer larger ones
- **Debounce persistence** - use `useDebouncedPersistence` for settings that change frequently
- **Stream large data** - don't load entire files into memory when streaming is possible
### Profiling
When investigating performance issues:
1. Use Chrome DevTools Performance tab (Cmd+Option+I → Performance)
2. Record during the slow operation
3. Look for long tasks (>50ms) blocking the main thread
4. Check for excessive re-renders in React DevTools Profiler
## Debugging Guide
### Focus Not Working

View File

@@ -49,10 +49,26 @@ export interface UseSortedSessionsReturn {
export function useSortedSessions(deps: UseSortedSessionsDeps): UseSortedSessionsReturn {
const { sessions, groups, bookmarksCollapsed } = deps;
// Helper to get worktree children for a session
const getWorktreeChildren = (parentId: string) =>
sessions.filter(s => s.parentSessionId === parentId)
.sort((a, b) => compareNamesIgnoringEmojis(a.worktreeBranch || a.name, b.worktreeBranch || b.name));
// Memoize worktree children lookup for O(1) access instead of O(n) per parent
// This reduces complexity from O(n²) to O(n) when building sorted sessions
const worktreeChildrenByParent = useMemo(() => {
const map = new Map<string, Session[]>();
for (const s of sessions) {
if (s.parentSessionId) {
const existing = map.get(s.parentSessionId);
if (existing) {
existing.push(s);
} else {
map.set(s.parentSessionId, [s]);
}
}
}
// Sort each group once
for (const [, children] of map) {
children.sort((a, b) => compareNamesIgnoringEmojis(a.worktreeBranch || a.name, b.worktreeBranch || b.name));
}
return map;
}, [sessions]);
// Create sorted sessions array that matches visual display order (includes ALL sessions)
// Note: sorting ignores leading emojis for proper alphabetization
@@ -60,7 +76,7 @@ export function useSortedSessions(deps: UseSortedSessionsDeps): UseSortedSession
const sortedSessions = useMemo(() => {
const sorted: Session[] = [];
// Helper to add session with its worktree children
// Helper to add session with its worktree children - now O(1) lookup
const addSessionWithWorktrees = (session: Session) => {
// Skip worktree children - they're added with their parent
if (session.parentSessionId) return;
@@ -69,8 +85,10 @@ export function useSortedSessions(deps: UseSortedSessionsDeps): UseSortedSession
// Add worktree children if expanded
if (session.worktreesExpanded !== false) {
const children = getWorktreeChildren(session.id);
sorted.push(...children);
const children = worktreeChildrenByParent.get(session.id);
if (children) {
sorted.push(...children);
}
}
};
@@ -90,8 +108,16 @@ export function useSortedSessions(deps: UseSortedSessionsDeps): UseSortedSession
ungroupedSessions.forEach(addSessionWithWorktrees);
return sorted;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessions, groups]);
}, [sessions, groups, worktreeChildrenByParent]);
// Create a Map for O(1) group lookup instead of O(n) find() calls
const groupsById = useMemo(() => {
const map = new Map<string, Group>();
for (const g of groups) {
map.set(g.id, g);
}
return map;
}, [groups]);
// Create visible sessions array for session jump shortcuts (Opt+Cmd+NUMBER)
// Order: Bookmarked sessions first (if bookmarks folder expanded), then groups/ungrouped
@@ -111,17 +137,18 @@ export function useSortedSessions(deps: UseSortedSessionsDeps): UseSortedSession
// Add sessions from expanded groups and ungrouped sessions
// Exclude worktree children (they don't show jump numbers)
// Use Map for O(1) group lookup instead of O(n) find()
const groupAndUngrouped = sortedSessions.filter(session => {
// Exclude worktree children - they're nested under parent and don't show jump badges
if (session.parentSessionId) return false;
if (!session.groupId) return true; // Ungrouped sessions always visible
const group = groups.find(g => g.id === session.groupId);
const group = groupsById.get(session.groupId);
return group && !group.collapsed; // Only show if group is expanded
});
result.push(...groupAndUngrouped);
return result;
}, [sortedSessions, groups, sessions, bookmarksCollapsed]);
}, [sortedSessions, groupsById, sessions, bookmarksCollapsed]);
return {
sortedSessions,