Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

@ammar-agent ammar-agent commented Oct 14, 2025

Summary

This PR optimizes startup performance and eliminates UI freezes through three key improvements:

  1. Lazy-load Mermaid - Defer 482KB Mermaid.js bundle until diagrams are rendered
  2. Refine IPC architecture - Move token stats calculation to frontend for better separation of concerns
  3. Fix UI freeze - Offload tokenization to worker thread to keep main process responsive

Changes

1. Bundle Size Optimization (Commit: 20634dc)

  • Lazy load Mermaid component - Converted to dynamic import() pattern
  • Add manual code splitting - Separate chunks for react-vendor and syntax-highlighter
  • Documentation - Added comprehensive bundle size analysis

Results:

  • Before: 2,833KB single bundle (all loaded on startup)
  • After: 2,292KB split across chunks
  • Savings: 541KB (19% reduction)
  • Mermaid: 482KB now loads on-demand only when diagrams are rendered

Performance Impact:

  • ⚡ 200-500ms faster startup (estimated)
  • 📉 Lower baseline memory usage when no diagrams present
  • 🌐 Network bandwidth saved - mermaid only downloaded when needed

2. IPC Architecture Refinement (Commit: 221e81e)

Inverted ownership for token consumer calculation - backend now only tokenizes, frontend handles display logic.

Before:

Frontend → IPC stats.calculate(workspaceId, model) → Backend calculates everything
                                                     (consumers, percentages, sorting)

After:

Frontend → IPC tokens.countBulk(model, texts) → Backend just tokenizes
Frontend calculates consumers, percentages, sorting

Benefits:

  • ✅ Simpler API - one generic endpoint vs specialized stats calculator
  • ✅ Better testability - pure functions, no IPC mocking needed
  • ✅ Clearer ownership - backend tokenizes, frontend calculates display logic
  • ✅ More flexible - frontend can customize calculation without backend changes

New Files:

  • src/utils/tokens/consumerCalculator.ts (159 LoC) - Pure calculation functions
  • src/utils/tokens/consumerCalculator.test.ts (240 LoC) - 12 comprehensive tests

Changes:

  • New IPC endpoint: tokens:countBulk(model, texts[])
  • Updated TokenConsumerBreakdown to calculate stats in frontend
  • All 528 tests pass + 12 new tests

3. Fix UI Freeze with Worker Thread (Commit: aac20c7)

The app was freezing for 3-4 seconds when first opening Costs tab because tokenizer loading and computation happened synchronously in main process, blocking all window events.

Solution: Move tokenization to Node.js worker thread

Architecture Change:

Before:

Renderer → IPC → Main Process (synchronous work) → FROZEN UI ❌
                  ↓
            Load tokenizer (3-4s)
            Count tokens (CPU work)

After:

Renderer → IPC → Main Process (spawns worker) → RESPONSIVE UI ✅
                      ↓
                 Worker Thread
                      ↓
                 Load tokenizer
                 Count tokens
                      ↓
                 Return results

New Files:

  • src/workers/tokenizerWorker.ts (53 LoC) - Worker thread for CPU-intensive tokenization
  • src/services/tokenizerWorkerPool.ts (161 LoC) - Manages worker lifecycle and request queue

Impact:

  • ✅ Main process never blocks - stays responsive during tokenization
  • ✅ No UI freeze - can interact with app while calculation runs
  • ✅ Same API - no frontend changes needed
  • ✅ Fast subsequent calls - worker stays loaded between requests

Testing

  • ✅ All 528 tests pass + 12 new tests
  • ✅ TypeScript compiles cleanly
  • ✅ ESLint passes (no new errors)
  • ✅ Build successful
  • ✅ Worker builds correctly to dist/src/workers/tokenizerWorker.js

Net Changes

8 files changed, 706 insertions(+), 36 deletions(-)

Added:

  • src/utils/tokens/consumerCalculator.ts (159 LoC)
  • src/utils/tokens/consumerCalculator.test.ts (240 LoC)
  • src/workers/tokenizerWorker.ts (53 LoC)
  • src/services/tokenizerWorkerPool.ts (161 LoC)
  • BUNDLE_SIZE_ANALYSIS.md (documentation)

Modified:

  • IPC handlers, preload, types
  • TokenConsumerBreakdown.tsx - now calculates stats in frontend
  • CostsTab.tsx - passes messages instead of workspaceId
  • tsconfig.main.json - includes workers and services

Documentation

Added BUNDLE_SIZE_ANALYSIS.md with:

  • Complete bundle size breakdown
  • Top 10 largest dependencies identified
  • Phase 1/2/3 optimization roadmap
  • Implementation notes and results

Next Optimization Opportunities

  1. Lazy load react-syntax-highlighter (~620KB savings)
  2. Disable production source maps (~50MB savings in .app bundle)
  3. Replace mermaid with lighter alternative (future consideration)

Generated with cmux

@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch from bd18b4a to b95402e Compare October 14, 2025 16:05
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch 8 times, most recently from ef0c006 to 2a907ec Compare October 14, 2025 16:56
Copy link
Collaborator Author

@ammar-agent ammar-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment referred to StreamingTokenTracker.ts which was removed from this PR during cleanup. The PR now only contains mermaid lazy loading and tokenizer changes.

@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch 2 times, most recently from 2a8acfc to 33b5b69 Compare October 15, 2025 01:22
@ammar-agent ammar-agent changed the title 🤖 Lazy load Mermaid to improve startup performance 🤖 Optimize startup performance and bundle size Oct 15, 2025
@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch 2 times, most recently from eb2f610 to 20634dc Compare October 15, 2025 01:46
@ammar-agent
Copy link
Collaborator Author

✅ Startup Freeze Fixed

Root Cause: AIService had a static import of the tokenizer that loaded 30MB+ of modules at app startup, blocking for 3-4 seconds.

// Line 31 - BLOCKING STARTUP
import { getTokenizerForModel } from "@/utils/main/tokenizer";

Import chain:

main.ts → ipcMain.ts → AIService → getTokenizerForModel (STATIC IMPORT)
                                         ↓
                                   30MB tokenizer modules load
                                         ↓
                                   3-4 SECOND FREEZE

The tokenizer was used to count system message tokens for a "System" consumer in the token breakdown UI.

Solution: Remove systemMessageTokens Entirely

Removed the calculation because:

  1. Redundant - API already includes system tokens in usage.inputTokens
  2. Limited value - System messages rarely change, users care more about user/assistant/tools breakdown
  3. Wrong layer - Recent refactor moved all token calculations to frontend, this was the last holdout
  4. Blocking startup - Only reason for eager tokenizer import

Changes (commit 77fe4bd)

  • ❌ Removed tokenizer import from AIService
  • ❌ Removed systemMessageTokens calculation (3 lines)
  • ❌ Removed all assignments of systemMessageTokens in metadata
  • ❌ Removed "System" consumer from tokenStatsCalculator
  • 📝 Marked field as deprecated in type definitions

Impact:

  • Fixes 3-4 second startup freeze
  • ✅ Removes only use of eager tokenizer loading
  • ✅ All 528 tests pass
  • ⚠️ "System" consumer no longer appears in token breakdown (can add back via frontend if needed)

Net LoC: -15

This completes the token calculation refactor - frontend now owns all breakdown logic via consumerCalculator.ts and worker threads.


Full PR Summary

Three performance fixes in this PR:

  1. IPC Architecture (221e81e) - Inverted token calculation ownership to frontend
  2. Worker Threads (aac20c7) - Moved tokenization off main thread
  3. Startup Freeze (77fe4bd) - Removed eager tokenizer import ✅ This commit

App now:

  • Starts instantly (no 3-4s freeze)
  • Stays responsive during tokenization (worker thread)
  • Token breakdown calculates on-demand (when user opens Costs tab)

@ammar-agent
Copy link
Collaborator Author

🎉 Startup Freeze Fixed!

Root Cause Analysis

The 8-10 second freeze was NOT the tokenizer. Investigation revealed:

Timing breakdown:

22:40:00.813  Services loaded (88ms) ✅ Main process fast
22:40:01.340  [RENDERER] Imports done: 0.1ms ✅ React bundle loads instantly
22:40:01.341  [RENDERER] Render called
22:40:01.378  useProjectManagement working
22:40:10.041  Git status done ← 8.7 SECOND FREEZE

The culprit: GitStatusStore.syncWorkspaces() was calling updateGitStatus() immediately in a useEffect, blocking the UI until all 4 projects' git operations completed.

Fixes Applied

1. Make Git Status Non-Blocking (Commit daa485c)

- // Run immediately
- void this.updateGitStatus();
+ // Run first update immediately but asynchronously (don't block UI)
+ // setTimeout ensures this runs on next tick, allowing React to finish rendering
+ setTimeout(() => void this.updateGitStatus(), 0);

Impact:

  • UI becomes interactive immediately
  • Git status icons still appear after ~8s (same total time, just non-blocking)
  • No functional changes to git status behavior

2. Remove Noisy Debug Logs (Commit 51e5c85)

Removed console spam from git status failures:

  • [gitStatus] Script failed for cmux-debug: [OUTPUT TRUNCATED...]
  • [fetch] Success for cmux
  • [fetch] Failed for...

Git operations fail regularly (large repos, network issues, timeouts). These are expected and retry automatically - no need to spam console.

Summary of All PR Changes

This PR now includes 3 performance optimizations:

  1. IPC Architecture (221e81e) - Moved token calculation to frontend
  2. Worker Threads (aac20c7) - Offloaded tokenization to worker thread
  3. Startup Freeze (77fe4bd + daa485c) - Removed tokenizer import + made git status non-blocking ✅

Total impact:

  • ✅ App starts instantly (no freeze)
  • ✅ Stays responsive during tokenization
  • ✅ Token breakdown calculates on-demand
  • ✅ Git status updates in background

Test it: make start - UI should be interactive immediately!

@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch 2 times, most recently from 928f3f2 to a84d060 Compare October 15, 2025 14:28
@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch 5 times, most recently from 4b5fbd5 to 7960652 Compare October 15, 2025 14:40
Reduces startup time from 8.6s to <500ms and eliminates the 3s "evil spinner"
that blocked the UI after the window appeared.

Root Cause Analysis:
- AIService was importing the massive "ai" package (~3s) during startup
- Git status IPC handler triggered AIService lazy-load immediately after mount
- Combined effect: window appeared but then froze for 3s before becoming interactive

Key Performance Fixes:
1. Lazy-load AIService in IpcMain - defer AI SDK import until first actual use
2. Fix git status IPC to use Config.findWorkspace() instead of AIService
3. Defer git status polling to next tick (non-blocking on mount)
4. Defer auto-resume checks to next tick (non-blocking on mount)
5. Make React DevTools installation non-blocking in main process

Token Stats Refactoring:
- Remove ~425 LoC of approximation infrastructure (tokenStats.worker, etc.)
- Move token calculation to backend via IPC (TOKENS_COUNT_BULK handler)
- Add tokenizerWorkerPool for non-blocking tokenization in main process
- Eliminate ChatProvider context - move stats to TokenConsumerBreakdown component
- Add per-model cache keys to prevent cross-model contamination
- Remove unused stats.calculate IPC endpoint (-170 LoC dead code)

Bundle Optimizations:
- Lazy load Mermaid component to defer 631KB chunk
- Disable production source maps (saves ~50MB in .app)
- Add manual chunks for better caching (react-vendor, syntax-highlighter)
- Remove worker bundle checks (worker removed in refactoring)

Import Linting Enhancements:
- Enhanced check_eager_imports.sh to detect AI SDK in critical startup files
- Added renderer/worker checks to prevent heavy packages (ai-tokenizer, models.json)
- CI guards for bundle sizes (400KB main budget)

Performance Results:
- Startup: 8.6s → <500ms (94% improvement)
- Window: appears instantly, no freeze
- AI SDK: loads on-demand when first message sent
- Git status: non-blocking background operation

Testing:
- All 546 tests pass (removed 1 test file for dead code)
- Integration tests for tokens.countBulk IPC handler
- Tokenizer cache isolation tests
- StreamingTokenTracker model-change safety tests

_Generated with `cmux`_
The error message changed when we stopped using AIService.getWorkspaceMetadata()
and started using Config.findWorkspace() directly (commit cdd3302).

Old: 'Failed to get workspace metadata'
New: 'Workspace not found: nonexistent-workspace'
The lazy-load getter was using require('@/services/aiService') which works
during development but fails in production because Node.js doesn't resolve
TypeScript path aliases at runtime.

Changed to require('./aiService') (relative path) which works both in
development and in the compiled dist/main.js.

This was causing E2E tests to fail - streams never completed because
AIService was never successfully instantiated in the built Electron app.
@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch from 318ec10 to 8f5daa2 Compare October 15, 2025 15:25
@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch from f609a86 to c62b5e9 Compare October 15, 2025 15:35
The tokenizerWorkerPool was trying to load from dist/workers/tokenizerWorker.js
but workers weren't being compiled because tsconfig.main.json didn't include them.

This caused silent failures in E2E tests when the token counting IPC endpoint
tried to initialize the worker pool, which likely affected stream event timing.
@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch 2 times, most recently from 5f554dd to e9f4b29 Compare October 15, 2025 16:04
@ammar-agent ammar-agent force-pushed the investigate-bundle-size branch from e9f4b29 to 8589123 Compare October 15, 2025 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants