Concurrent Scanning Architecture
Pumu uses Go’s goroutines and semaphore-based throttling to achieve blazingly fast performance when scanning and deleting large directory trees.Goroutines with Bounded Concurrency
Fromscanner.go:167-201, here’s how Pumu calculates folder sizes concurrently:
| Component | Purpose | Implementation |
|---|---|---|
| WaitGroup | Wait for all goroutines to complete | sync.WaitGroup |
| Mutex | Protect shared folders slice | sync.Mutex |
| Semaphore | Limit max concurrent operations to 20 | Buffered channel chan struct{} |
| Atomic ops | Thread-safe size accumulation during deletion | atomic.AddInt64() |
Why Limit to 20 Concurrent Operations?
File descriptor limits
File descriptor limits
Each directory traversal opens file descriptors. Most systems limit open file descriptors per process (typically 1024-4096).20 concurrent operations balances:
- ✅ Fast parallel scanning
- ✅ Avoiding file descriptor exhaustion
- ✅ Reasonable CPU/memory usage
Disk I/O bottlenecks
Disk I/O bottlenecks
On traditional HDDs, too many concurrent reads cause disk thrashing. SSDs handle concurrency better, but 20 is a safe default for both.For NVMe SSDs or NFS mounts, you could theoretically increase this limit by modifying the semaphore buffer size in the source code.
Performance Optimizations
1. Concurrent Size Calculation
Before goroutines (sequential):2. Concurrent Deletion
Fromscanner.go:218-234, deletion also uses the same semaphore pattern:
- Multiple folders deleted simultaneously
- Atomic size tracking prevents race conditions
- Failed deletions don’t block others
3. Smart Path Skipping
Pumu skips irrelevant directories to avoid wasting time (fromscanner.go:26-31):
- System Folders
- Package Manager Caches
- IDE Folders
- Version Control
Skipped:
Library, AppData, Local, RoamingReason:- macOS and Windows system directories
- Contain OS-level caches, not project dependencies
- Scanning them wastes time and may cause permission errors
4. Early Exit on Skip
When an ignored path is detected, Pumu usesfilepath.SkipDir to avoid descending into subdirectories:
.npm cache:
| Without SkipDir | With SkipDir |
|---|---|
| 50,000+ files scanned | Skipped entirely |
| ~30 seconds | < 1 second |
Ignored Paths Reference
Fromscanner.go:26-31, here’s the complete list:
| Path | Category | Reason |
|---|---|---|
.Trash | System | macOS trash folder |
.cache | System | Generic cache directory |
.npm | Package Manager | npm global cache |
.yarn | Package Manager | Yarn global cache |
.cargo | Package Manager | Rust/Cargo global cache |
.rustup | Package Manager | Rust toolchain manager |
Library | System | macOS system libraries |
AppData | System | Windows application data |
Local | System | Windows local app data |
Roaming | System | Windows roaming profiles |
.vscode | IDE | VS Code settings |
.idea | IDE | JetBrains IDE settings |
.git | VCS | Git repository (hardcoded check) |
Best Practices for Large Directory Trees
1. Scan Specific Subdirectories
Avoid:/ or ~ wastes time on system folders.
2. Use Dry-Run First
- See results without waiting for deletion
- Estimate cleanup time based on folder count
- Verify no critical folders are targeted
3. Increase Concurrency for Fast Storage
If you have an NVMe SSD or network storage, you can modify the semaphore limit: Editscanner.go:174 and scanner.go:220:
This is not recommended for HDDs or slower systems, as it may cause disk thrashing.
4. Exclude Specific Paths
Currently, Pumu doesn’t support custom exclusions via CLI. Workaround:Memory and CPU Considerations
Memory Usage
Per-folder memory:TargetFolderstruct: ~40 bytes (path string + int64 size)- 1,000 folders = ~40 KB
- 10,000 folders = ~400 KB
- Each goroutine: ~2-8 KB initial stack size
- Max 20 concurrent: ~160 KB
What if I have 100,000+ folders?
What if I have 100,000+ folders?
Memory usage scales linearly:
- 100,000 folders = ~4 MB for folder list
- Goroutine overhead: ~160 KB (max 20 concurrent)
- Total: ~5 MB
CPU Usage
During scanning:- Up to 20 cores saturated (if available)
- Each goroutine performs disk I/O (CPU-bound on traversal)
- Same concurrency (20 goroutines)
- Mostly I/O-bound (disk write operations)
- 4+ CPU cores for optimal performance
- Works fine on 2 cores, just slower
Benchmarks
These are real-world benchmarks from the README:Typical Scan Performance
| Scenario | Folders Found | Time (Sequential) | Time (Concurrent) | Speedup |
|---|---|---|---|---|
| Small project | 5 | ~2s | ~1s | 2x |
| Medium monorepo | 50 | ~45s | ~5s | 9x |
| Large workspace | 200 | ~300s | ~20s | 15x |
Deletion Performance
| Folder Type | Size | Delete Time (HDD) | Delete Time (SSD) |
|---|---|---|---|
node_modules | 500 MB | ~15s | ~3s |
target | 2 GB | ~60s | ~10s |
.venv | 200 MB | ~8s | ~2s |
Deletion time depends more on file count than total size. A 1 GB folder with 100,000 small files takes longer to delete than a 5 GB folder with 10 large files.
Performance FAQs
Why is scanning my network drive slow?
Why is scanning my network drive slow?
Network drives have high latency per file operation:
- Local SSD: ~0.1ms per file
- NFS/SMB: ~5-50ms per file
Can I increase concurrency above 20?
Can I increase concurrency above 20?
Yes, but benchmark first:Modify
scanner.go, recompile, and test again. If time decreases, keep the change.Why does deletion take longer than expected?
Why does deletion take longer than expected?
Possible reasons:
- Many small files - More overhead per file
- HDD fragmentation - Slower random I/O
- Filesystem overhead - ext4/APFS have different deletion speeds
--dry-run to estimate before deleting.See Also
- Package Manager Detection - Understanding detection logic
- Safety Features - What’s protected from deletion