## Summary Add a `--profile` flag and `npm run profile` scripts to make CPU and memory profiling routine, lowering the barrier for performance investigation and optimization. ## Problem Statement Performance profiling currently requires manual Node.js tooling: **Current approach (manual):** ```bash # CPU profiling node --inspect bin/copytree.js /path/to/project # Then: Open chrome://inspect, manually start recording # Heap profiling node --inspect --expose-gc bin/copytree.js /path/to/project # Then: Take heap snapshots manually via DevTools # External tools npm install -g clinic clinic doctor -- node bin/copytree.js /path/to/project ``` **Problems:** - High friction for contributors and users - No standardized profiling workflow - Results not saved/tracked over time - No integration with existing performance utilities **Blocked scenarios:** - Developer wants to profile their use case quickly - Contributor investigating performance regression - User reporting "CopyTree is slow" needs data - Automated performance monitoring in CI ## User Story **As a** CopyTree contributor or power user **I want** built-in profiling with one command **So that** I can investigate performance issues without manual Node.js tooling ## Expected Behavior ### CLI Profiling ```bash # CPU profiling (saves .cpuprofile) copytree --profile cpu /path/to/project # Heap profiling (saves .heapprofile) copytree --profile heap /path/to/project # Both CPU + heap copytree --profile all /path/to/project # Custom output directory copytree --profile cpu --profile-dir ./profiling /path/to/project ``` ### NPM Scripts ```bash # Development profiling npm run profile # Runs on fixtures, saves to .profiles/ npm run profile:cpu # CPU only npm run profile:heap # Heap only npm run profile:compare # Compare with baseline ``` ### Output Structure ``` .profiles/ ├── 2024-01-15T10-30-45-cpu.cpuprofile ├── 2024-01-15T10-30-45-heap.heapprofile ├── 2024-01-15T10-30-45-report.json └── baseline/ ├── cpu.cpuprofile └── report.json ``` ### Report Format ```json { "timestamp": "2024-01-15T10:30:45.123Z", "version": "0.13.1", "command": "copytree /project --format json", "duration": 1523, "files": { "total": 1234, "processed": 1234, "excluded": 45 }, "memory": { "heapUsed": 125829120, "heapTotal": 185237504, "external": 2048576, "rss": 256000000 }, "stages": [ {"name": "FileDiscoveryStage", "duration": 523, "memoryDelta": 15728640}, {"name": "TransformStage", "duration": 645, "memoryDelta": 52428800}, {"name": "OutputFormattingStage", "duration": 355, "memoryDelta": 8388608} ], "profileFiles": { "cpu": ".profiles/2024-01-15T10-30-45-cpu.cpuprofile", "heap": ".profiles/2024-01-15T10-30-45-heap.heapprofile" } } ``` ## Current State **Existing performance utilities:** - [`src/utils/performance.js`](https://github.com/gregpriday/copytree/blob/develop/src/utils/performance.js) - Performance monitoring utilities - [`src/utils/performanceBudgets.js`](https://github.com/gregpriday/copytree/blob/develop/src/utils/performanceBudgets.js) - Budget enforcement - [`tests/performance/benchmark.js`](https://github.com/gregpriday/copytree/blob/develop/tests/performance/benchmark.js) - Manual benchmarking **Existing metrics collection:** - Pipeline already tracks stage durations ([`Pipeline.js`](https://github.com/gregpriday/copytree/blob/develop/src/pipeline/Pipeline.js)) - Memory deltas captured per stage - File count statistics available **Gaps:** - No CPU profiler integration - No heap profiler integration - No automatic profile file generation - No comparison with baselines ## Proposed Solution ### 1. Add Profiling Module New file: `src/utils/profiler.js` ```javascript import { Session } from 'inspector'; import fs from 'fs-extra'; import path from 'path'; export class Profiler { constructor(options = {}) { this.session = new Session(); this.profileDir = options.profileDir || '.profiles'; this.type = options.type || 'cpu'; // 'cpu', 'heap', 'all' this.timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0]; } async start() { this.session.connect(); if (this.type === 'cpu' || this.type === 'all') { this.session.post('Profiler.enable'); this.session.post('Profiler.start'); } if (this.type === 'heap' || this.type === 'all') { this.session.post('HeapProfiler.enable'); this.session.post('HeapProfiler.startSampling'); } } async stop() { const results = {}; if (this.type === 'cpu' || this.type === 'all') { const profile = await new Promise((resolve) => { this.session.post('Profiler.stop', (err, { profile }) => { resolve(profile); }); }); const cpuPath = path.join(this.profileDir, `${this.timestamp}-cpu.cpuprofile`); await fs.ensureDir(this.profileDir); await fs.writeJson(cpuPath, profile); results.cpu = cpuPath; } if (this.type === 'heap' || this.type === 'all') { const profile = await new Promise((resolve) => { this.session.post('HeapProfiler.stopSampling', (err, { profile }) => { resolve(profile); }); }); const heapPath = path.join(this.profileDir, `${this.timestamp}-heap.heapprofile`); await fs.writeJson(heapPath, profile); results.heap = heapPath; } this.session.disconnect(); return results; } } ``` ### 2. Integrate with CLI ```javascript // In bin/copytree.js .option('--profile <type>', 'Enable profiling: cpu, heap, all') .option('--profile-dir <dir>', 'Profile output directory (default: .profiles)') ``` ### 3. Wrap copyCommand ```javascript // In src/commands/copy.js async function copyCommand(targetPath, options) { let profiler; if (options.profile) { profiler = new Profiler({ type: options.profile, profileDir: options.profileDir }); await profiler.start(); } const startTime = Date.now(); const result = await /* ... existing logic ... */; if (profiler) { const profileFiles = await profiler.stop(); // Save report const report = { timestamp: new Date().toISOString(), version: pkg.version, duration: Date.now() - startTime, files: result.stats, memory: process.memoryUsage(), stages: result.stageTimings, profileFiles }; const reportPath = path.join( options.profileDir || '.profiles', `${profiler.timestamp}-report.json` ); await fs.writeJson(reportPath, report, { spaces: 2 }); logger.success(`Profile saved: ${reportPath}`); } return result; } ``` ### 4. Add NPM Scripts ```json { "scripts": { "profile": "node scripts/profile.js", "profile:cpu": "npm run profile -- --type cpu", "profile:heap": "npm run profile -- --type heap", "profile:compare": "node scripts/profile-compare.js" } } ``` ### 5. Viewing Profiles **CPU profiles (Chrome DevTools):** ```bash # 1. Generate profile copytree --profile cpu /project # 2. Open Chrome DevTools # 3. Go to Performance tab → Load Profile # 4. Select .profiles/*-cpu.cpuprofile ``` **Heap profiles (Chrome DevTools):** ```bash # Similar process in Memory tab ``` **Speedscope (alternative viewer):** ```bash npm install -g speedscope speedscope .profiles/*-cpu.cpuprofile ``` ## Tasks - [ ] Create `src/utils/profiler.js` with CPU and heap profiling - [ ] Add `--profile <type>` flag to [`bin/copytree.js`](https://github.com/gregpriday/copytree/blob/develop/bin/copytree.js) - [ ] Add `--profile-dir <dir>` flag for custom output location - [ ] Integrate profiler into [`copyCommand`](https://github.com/gregpriday/copytree/blob/develop/src/commands/copy.js) - [ ] Generate JSON report with metrics + profile file paths - [ ] Create `scripts/profile.js` for npm run profile - [ ] Create `scripts/profile-compare.js` for baseline comparison - [ ] Add `.profiles/` to `.gitignore` - [ ] Add baseline profiles to `.profiles/baseline/` - [ ] Write tests for profiler module - [ ] Add profiling section to `CONTRIBUTING.md` - [ ] Document profile viewer options (Chrome DevTools, Speedscope) - [ ] Create example profile analysis in docs ## Acceptance Criteria - [ ] `copytree --profile cpu` generates valid `.cpuprofile` file - [ ] `copytree --profile heap` generates valid `.heapprofile` file - [ ] `copytree --profile all` generates both CPU and heap profiles - [ ] Profile files open correctly in Chrome DevTools - [ ] JSON report includes: duration, memory, stages, profile paths - [ ] `npm run profile` runs profiling on fixture project - [ ] `npm run profile:compare` shows diff vs baseline - [ ] `.profiles/` directory created automatically - [ ] Profiling works with all CLI options (--format, --stream, etc.) - [ ] Profiling overhead < 10% (minimal impact on measurements) - [ ] Documentation includes screenshot of DevTools with loaded profile ## Additional Context **Node.js Inspector API:** - Built-in module: `const { Session } = require('inspector')` - No external dependencies needed - Same output as Chrome DevTools manual profiling **Chrome DevTools workflow:** 1. Open `chrome://inspect` 2. Click "Open dedicated DevTools for Node" 3. Performance tab → Load Profile 4. Import `.cpuprofile` file 5. Analyze flame graph, call tree, bottom-up **Speedscope (alternative):** - Web-based flamegraph viewer - Install: `npm install -g speedscope` - Usage: `speedscope profile.cpuprofile` - URL: https://www.speedscope.app/ **Clinic.js (future consideration):** - `clinic doctor` - Event loop delays - `clinic bubbleprof` - Async operations - `clinic flame` - Flamegraphs - Could integrate as optional dependency **Baseline tracking:** ```bash # Set baseline (after optimizations) npm run profile cp .profiles/latest-report.json .profiles/baseline/report.json # Compare current vs baseline npm run profile:compare # Output: +15% slower than baseline (regression detected) ``` **CI integration (future):** ```yaml # .github/workflows/performance.yml - name: Profile on main branch run: npm run profile - name: Compare vs baseline run: | npm run profile:compare # Fail if regression > 20% ``` **Related files:** - [`src/utils/performance.js`](https://github.com/gregpriday/copytree/blob/develop/src/utils/performance.js) - Existing performance utilities - [`tests/performance/benchmark.js`](https://github.com/gregpriday/copytree/blob/develop/tests/performance/benchmark.js) - Benchmark infrastructure **Example usage:** ```bash # Quick profiling copytree --profile cpu ~/large-project # Detailed profiling with custom output copytree --profile all --profile-dir ./debug/profiles ~/project # Development workflow npm run profile # Profile on fixtures npm run profile:cpu # CPU only npm run profile:compare # Check for regressions ```