mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## 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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user