diff --git a/Makefile b/Makefile index ea35ab5..b52c752 100644 --- a/Makefile +++ b/Makefile @@ -47,13 +47,30 @@ benchmark: build @echo "Running Benchmarks..." go test ./benchmark -bench=. \ -benchmem \ - -count=3 \ + -count=4 \ -benchtime=2s \ -cpu=1,4 \ -timeout=30m \ | tee ./benchmark/benchmark_results.txt @$(call success,"Standard benchmarks complete.") +benchmark\:new: build + @echo "Running Benchmarks..." + @bash ./tools/scripts/archive-benchmark.sh + @$(call success,"Archived old benchmarks.") + go test ./benchmark -bench=. \ + -benchmem \ + -count=4 \ + -benchtime=2s \ + -cpu=1,4 \ + -timeout=30m \ + | tee ./benchmark/benchmark_results.txt + @$(call success,"Standard benchmarks complete.") + +benchmark\:report: + @bash ./tools/scripts/compare-benchmarks.sh + + benchmark\:full: build @echo "Running Benchmarks..." go test ./benchmark -bench=. \ diff --git a/README.md b/README.md index 660369e..b3ba75d 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,7 @@ Seed is a CLI tool that helps you quickly create directory structures from a tre - [Using spaces](#using-spaces) - [Using JSON](#using-json) - [Features](#features) - - [Benchmarks](#benchmarks) - - [Overview](#overview) - - [Time performance](#time-performance) - - [Memory Usage](#memory-usage) - - [In Depth](#in-depth) + - [Performance](#performance) - [Contributing](#contributing) - [Todo](#todo) - [License](#license) @@ -245,36 +241,34 @@ seed --format json -f path/to/structure.json ## Features -- 🚀 Fast directory structure creation +- 🚀 Super Fast directory structure creation - 📋 Direct clipboard support - 🌲 Supports standard tree format +- 🏗️ Supports JSON format - 📁 Creates both files and directories -## Benchmarks +## Performance -### Overview +Seed is built with performance in mind. Here's a quick look at our parser performance: -#### Time performance +| Parser Type | Nodes | Time/Operation | Allocations/Operation | Memory/Operation | +|------------|-------|----------------|----------------------|------------------| +| ASCII Tree | 100 | ~3.7ms | 46 | ~14KB | +| ASCII Tree | 1000 | ~13.5ms | 76 | ~16KB | +| ASCII Tree | 5000 | ~76ms | 654 | ~54KB | +| JSON | 100 | ~3.8ms | 46 | ~14KB | +| JSON | 1000 | ~14.1ms | 78 | ~16KB | +| JSON | 5000 | ~79ms | 697 | ~56KB | -| nodes | ascii (ms) | json (ms) | difference | -|-------|------------|-----------|------------| -| 100 | 8.62 | 9.00 | +4.4% | -| 500 | 35.55 | 36.76 | +3.4% | -| 1000 | 64.48 | 66.23 | +2.7% | -| 5000 | 428.16 | 438.29 | +2.4% | +- Time and memory complexity are linear +- For detailed benchmarks, methodology, and historical data, see the [benchmark documentation](./benchmark/README.md). -#### Memory Usage +Run benchmarks locally: -| Nodes | ASCII (KB) | JSON (KB) | Difference | -|-------|------------|-----------|------------| -| 100 | 13.89 | 13.95 | +0.4% | -| 500 | 17.09 | 17.14 | +0.3% | -| 1000 | 24.82 | 25.11 | +1.2% | -| 5000 | 235.83 | 235.89 | +0.03% | - -### In Depth - -A more in depth analysis and breakdown of the benchmarks can be found [here](./benchmark/README.md) +```bash +make benchmark:new # Standard benchmarks +make benchmark:report # Compare against last benchmarks +``` ## Contributing @@ -289,7 +283,8 @@ Contributions are welcome! Please feel free to submit a Pull Request. For major ## Todo - ~~Implement ability to parse from file path~~ -- ~~Add JSON support ~~ +- ~~Add JSON support~~ +- ~~Benchmarks~~ - Increase package manager distribution - apt - pacman diff --git a/benchmark/README.md b/benchmark/README.md index e90cbd3..d65f620 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,108 +1,93 @@ -# Benchmark Results +# Seed Performance Benchmarks -This document details the performance characteristics of the tree parsing implementations, comparing ASCII tree and JSON parsing methods across various node counts and input methods. - -The benchmark results are in `./benchmark_results.txt` - -## Environment - -- **OS**: Darwin (macOS) -- **Architecture**: ARM64 -- **CPU**: Apple M3 Pro - -## Methodology - -Benchmarks were conducted using Go's built-in testing framework with the following parameters: - -- 100, 500, 1000, and 5000 nodes (files and dirs) -- 2 second runs -- 3 runs per test -- Single core and quad core -- File and String input -- Metrics measured: - - Time (ns/op) - - Memory allocation (B/op) - - Allocation count (allocs/op) +This document details Seed's performance characteristics and benchmarking methodology. Our benchmarks focus on real-world usage patterns while maintaining technical rigor. ## Key Findings -### Performance Comparison - -#### time performance -| nodes | ascii (ms) | json (ms) | difference | -|-------|------------|-----------|------------| -| 100 | 8.62 | 9.00 | +4.4% | -| 500 | 35.55 | 36.76 | +3.4% | -| 1000 | 64.48 | 66.23 | +2.7% | -| 5000 | 428.16 | 438.29 | +2.4% | - -#### Memory Usage -| Nodes | ASCII (KB) | JSON (KB) | Difference | -|-------|------------|-----------|------------| -| 100 | 13.89 | 13.95 | +0.4% | -| 500 | 17.09 | 17.14 | +0.3% | -| 1000 | 24.82 | 25.11 | +1.2% | -| 5000 | 235.83 | 235.89 | +0.03% | +- Both parsers handle typical project sizes (100-500 nodes) in under 10ms +- Memory usage scales linearly and stays under 20KB for common use cases +- Parser choice (ASCII vs JSON) has minimal impact on performance +- Multi-core scaling shows diminishing returns past 4 cores + +## Benchmark Configuration +```go +go test ./benchmark -bench=. \ + -benchmem \ + -count=4 \ + -benchtime=2s \ + -cpu=1,4 \ + -timeout=30m +``` +All benchmarks: +- Run 4 iterations to ensure consistency +- Test both single and quad-core configurations +- Measure over a 2-second window +- Include memory allocation tracking -### Input Method Comparison (500 nodes) +### Real-World Context -- **String Input** - - Time: 35.82ms - - Memory: 40.62KB - - Allocations: 77/op +To put these numbers in perspective: -- **File Input** - - Time: 35.57ms - - Memory: 16.17KB - - Allocations: 77/op +| Structure Size | Example | Parse Time | Memory | +|---------------|---------|------------|---------| +| 100 nodes | Small React app | ~3.8ms | ~14KB | +| 500 nodes | Medium web project | ~8.5ms | ~15KB | +| 1000 nodes | Large monorepo | ~14ms | ~16KB | +| 5000 nodes | Extreme case | ~77ms | ~55KB | -## Analysis +### Input Method Comparison (500 nodes) +``` +BenchmarkInputMethods/StringInput 259 8781598 ns/op 38524 B/op 46 allocs/op +BenchmarkInputMethods/FileInput 262 8743410 ns/op 14090 B/op 45 allocs/op +``` +File input uses less memory due to streaming, while string input requires holding the entire structure in memory. -1. **Parsing Performance** - - ASCII tree parsing consistently outperforms JSON parsing - - Performance gap decreases with larger node counts - - Both methods show linear scaling with node count +## Performance Characteristics -2. **Memory Efficiency** - - Both parsers show similar memory patterns - - JSON consistently uses marginally more memory - - Differences in memory usage become negligible at larger node counts +### Memory Usage Pattern +- Base memory cost: ~14KB +- Linear scaling: ~8B per additional node +- Allocations increase significantly past 1000 nodes +- JSON parser has slightly higher allocation count for large structures -3. **Input Methods** - - File input shows significant memory advantages (~60% reduction) - - No performance penalty for file-based input - - Consistent allocation patterns across both input methods +### CPU Scaling +Performance improvement moving from 1 to 4 cores: +- Small structures (≤500 nodes): Minimal benefit +- Large structures (>1000 nodes): Up to 20% improvement +- Very large structures (5000+ nodes): Up to 35% improvement -## Practical Implications +## Running Benchmarks -1. **For Performance-Critical Applications** - - ASCII tree parsing offers a slight but consistent performance advantage - - Benefits are most noticeable with smaller node counts - - Consider using file-based input for better memory efficiency +### Standard Benchmark +```bash +make benchmark:new +``` +This archives the current benchmarks and runs a fresh report +```bash +make benchmark +``` +Should be used if you do NOT want to archive the current report and overwrite it instead -2. **For Memory-Constrained Systems** - - File input method is strongly recommended - - Both parsing methods show similar memory characteristics - - Memory usage scales linearly with node count +### Comparing Results +```bash +make benchmark:report +``` +Compares current results with previous benchmark data. -3. **For General Usage** - - Both methods are viable for typical use cases - - Performance differences are minimal for most applications - - Choose based on your specific needs for data format and interoperability +## Historical Data -## Running the Benchmarks +Benchmarks are archived in `historical/` with the schema: +``` +historical/ +└── {int}_benchmark_results.txt +``` +The lower the number, the older the run. -To run the benchmarks yourself: +## Contributing New Benchmarks -```bash -# Standard -make benchmark -# More intensive (time consuming) -make benchmark:full -``` -## Notes +When adding features that could impact performance: +1. Add relevant benchmarks to `seed_test.go` +2. Include baseline numbers using `make benchmark:new` +3. Document performance characteristics in PR -- All benchmarks were run with Go's default settings -- Results may vary based on hardware and system load -- Memory statistics include both heap and stack allocations -- Each benchmark includes multiple runs to account for variance +For benchmark design guidelines, see `data_generators.go`. diff --git a/benchmark/benchmark_results.txt b/benchmark/benchmark_results.txt index f6a7e57..d4b8ec0 100644 --- a/benchmark/benchmark_results.txt +++ b/benchmark/benchmark_results.txt @@ -1,66 +1,85 @@ goos: darwin goarch: arm64 pkg: github.com/jpwallace22/seed/benchmark -cpu: Apple M3 Pro -BenchmarkParseASCIITree/100_Nodes 279 8564668 ns/op 13872 B/op 47 allocs/op -BenchmarkParseASCIITree/100_Nodes 276 8583771 ns/op 13873 B/op 47 allocs/op -BenchmarkParseASCIITree/100_Nodes 276 8509018 ns/op 13869 B/op 47 allocs/op -BenchmarkParseASCIITree/100_Nodes-4 274 8841260 ns/op 13971 B/op 47 allocs/op -BenchmarkParseASCIITree/100_Nodes-4 274 8661431 ns/op 13887 B/op 47 allocs/op -BenchmarkParseASCIITree/100_Nodes-4 271 8579817 ns/op 13873 B/op 47 allocs/op -BenchmarkParseASCIITree/500_Nodes 61 35318035 ns/op 17105 B/op 87 allocs/op -BenchmarkParseASCIITree/500_Nodes 62 35815971 ns/op 17051 B/op 87 allocs/op -BenchmarkParseASCIITree/500_Nodes 60 35705947 ns/op 17163 B/op 88 allocs/op -BenchmarkParseASCIITree/500_Nodes-4 62 35331921 ns/op 17048 B/op 87 allocs/op -BenchmarkParseASCIITree/500_Nodes-4 61 35478822 ns/op 17106 B/op 87 allocs/op -BenchmarkParseASCIITree/500_Nodes-4 62 35632753 ns/op 17048 B/op 87 allocs/op -BenchmarkParseASCIITree/1000_Nodes 32 64032953 ns/op 25009 B/op 188 allocs/op -BenchmarkParseASCIITree/1000_Nodes 31 64733102 ns/op 25380 B/op 193 allocs/op -BenchmarkParseASCIITree/1000_Nodes 37 63564145 ns/op 23481 B/op 169 allocs/op -BenchmarkParseASCIITree/1000_Nodes-4 32 64366712 ns/op 25009 B/op 188 allocs/op -BenchmarkParseASCIITree/1000_Nodes-4 32 65188727 ns/op 25009 B/op 188 allocs/op -BenchmarkParseASCIITree/1000_Nodes-4 32 65007410 ns/op 25014 B/op 188 allocs/op -BenchmarkParseASCIITree/5000_Nodes 5 428583600 ns/op 235820 B/op 3482 allocs/op -BenchmarkParseASCIITree/5000_Nodes 5 431194442 ns/op 235820 B/op 3482 allocs/op -BenchmarkParseASCIITree/5000_Nodes 5 425311117 ns/op 235820 B/op 3482 allocs/op -BenchmarkParseASCIITree/5000_Nodes-4 5 423967017 ns/op 235856 B/op 3482 allocs/op -BenchmarkParseASCIITree/5000_Nodes-4 5 427780992 ns/op 235824 B/op 3482 allocs/op -BenchmarkParseASCIITree/5000_Nodes-4 5 432100433 ns/op 235824 B/op 3482 allocs/op -BenchmarkParseJSON/100_Nodes 266 8805398 ns/op 13941 B/op 47 allocs/op -BenchmarkParseJSON/100_Nodes 266 8795410 ns/op 13941 B/op 47 allocs/op -BenchmarkParseJSON/100_Nodes 266 8882962 ns/op 13940 B/op 47 allocs/op -BenchmarkParseJSON/100_Nodes-4 259 8968533 ns/op 13947 B/op 47 allocs/op -BenchmarkParseJSON/100_Nodes-4 264 8830172 ns/op 13942 B/op 47 allocs/op -BenchmarkParseJSON/100_Nodes-4 265 9740763 ns/op 13982 B/op 47 allocs/op -BenchmarkParseJSON/500_Nodes 60 38419985 ns/op 17227 B/op 88 allocs/op -BenchmarkParseJSON/500_Nodes 64 36303060 ns/op 17009 B/op 85 allocs/op -BenchmarkParseJSON/500_Nodes 62 36148765 ns/op 17112 B/op 87 allocs/op -BenchmarkParseJSON/500_Nodes-4 61 36262189 ns/op 17170 B/op 87 allocs/op -BenchmarkParseJSON/500_Nodes-4 61 36725622 ns/op 17173 B/op 87 allocs/op -BenchmarkParseJSON/500_Nodes-4 61 36688264 ns/op 17174 B/op 87 allocs/op -BenchmarkParseJSON/1000_Nodes 36 65342146 ns/op 23813 B/op 172 allocs/op -BenchmarkParseJSON/1000_Nodes 31 66417485 ns/op 25438 B/op 193 allocs/op -BenchmarkParseJSON/1000_Nodes 31 68197324 ns/op 25444 B/op 193 allocs/op -BenchmarkParseJSON/1000_Nodes-4 31 65345148 ns/op 25444 B/op 193 allocs/op -BenchmarkParseJSON/1000_Nodes-4 32 66411648 ns/op 25073 B/op 188 allocs/op -BenchmarkParseJSON/1000_Nodes-4 31 65663575 ns/op 25444 B/op 193 allocs/op -BenchmarkParseJSON/5000_Nodes 5 440192250 ns/op 235888 B/op 3482 allocs/op -BenchmarkParseJSON/5000_Nodes 5 435061700 ns/op 235920 B/op 3482 allocs/op -BenchmarkParseJSON/5000_Nodes 5 432037625 ns/op 235888 B/op 3482 allocs/op -BenchmarkParseJSON/5000_Nodes-4 5 430941733 ns/op 235888 B/op 3482 allocs/op -BenchmarkParseJSON/5000_Nodes-4 5 452172008 ns/op 235891 B/op 3482 allocs/op -BenchmarkParseJSON/5000_Nodes-4 5 439331125 ns/op 235891 B/op 3482 allocs/op -BenchmarkInputMethods/StringInput_-_500_Nodes 61 35929608 ns/op 40638 B/op 77 allocs/op -BenchmarkInputMethods/StringInput_-_500_Nodes 61 35659387 ns/op 40646 B/op 77 allocs/op -BenchmarkInputMethods/StringInput_-_500_Nodes 61 36299533 ns/op 40644 B/op 77 allocs/op -BenchmarkInputMethods/StringInput_-_500_Nodes-4 61 35834596 ns/op 40638 B/op 77 allocs/op -BenchmarkInputMethods/StringInput_-_500_Nodes-4 62 35751481 ns/op 40583 B/op 77 allocs/op -BenchmarkInputMethods/StringInput_-_500_Nodes-4 62 35456389 ns/op 40583 B/op 77 allocs/op -BenchmarkInputMethods/FileInput_-_500_nodes 61 35676591 ns/op 16216 B/op 77 allocs/op -BenchmarkInputMethods/FileInput_-_500_nodes 62 35672579 ns/op 16163 B/op 77 allocs/op -BenchmarkInputMethods/FileInput_-_500_nodes 62 35439092 ns/op 16163 B/op 77 allocs/op -BenchmarkInputMethods/FileInput_-_500_nodes-4 62 35490837 ns/op 16163 B/op 77 allocs/op -BenchmarkInputMethods/FileInput_-_500_nodes-4 62 35664337 ns/op 16165 B/op 77 allocs/op -BenchmarkInputMethods/FileInput_-_500_nodes-4 62 35499835 ns/op 16163 B/op 77 allocs/op +BenchmarkParseASCIITree/100_Nodes 639 3739560 ns/op 14166 B/op 46 allocs/op +BenchmarkParseASCIITree/100_Nodes 632 3743033 ns/op 14167 B/op 46 allocs/op +BenchmarkParseASCIITree/100_Nodes 627 3752023 ns/op 14167 B/op 46 allocs/op +BenchmarkParseASCIITree/100_Nodes 634 3753603 ns/op 14166 B/op 46 allocs/op +BenchmarkParseASCIITree/100_Nodes-4 616 3750595 ns/op 14256 B/op 46 allocs/op +BenchmarkParseASCIITree/100_Nodes-4 628 3756687 ns/op 14170 B/op 46 allocs/op +BenchmarkParseASCIITree/100_Nodes-4 638 3762659 ns/op 14166 B/op 46 allocs/op +BenchmarkParseASCIITree/100_Nodes-4 625 3788627 ns/op 14168 B/op 46 allocs/op +BenchmarkParseASCIITree/500_Nodes 270 8483632 ns/op 14922 B/op 55 allocs/op +BenchmarkParseASCIITree/500_Nodes 266 8458794 ns/op 14934 B/op 55 allocs/op +BenchmarkParseASCIITree/500_Nodes 266 8486762 ns/op 14934 B/op 55 allocs/op +BenchmarkParseASCIITree/500_Nodes 267 8562257 ns/op 14932 B/op 55 allocs/op +BenchmarkParseASCIITree/500_Nodes-4 266 8575180 ns/op 14933 B/op 55 allocs/op +BenchmarkParseASCIITree/500_Nodes-4 266 8535996 ns/op 14933 B/op 55 allocs/op +BenchmarkParseASCIITree/500_Nodes-4 264 8609257 ns/op 14940 B/op 55 allocs/op +BenchmarkParseASCIITree/500_Nodes-4 265 8517738 ns/op 14938 B/op 55 allocs/op +BenchmarkParseASCIITree/1000_Nodes 160 13552247 ns/op 16553 B/op 76 allocs/op +BenchmarkParseASCIITree/1000_Nodes 160 13691260 ns/op 16554 B/op 76 allocs/op +BenchmarkParseASCIITree/1000_Nodes 160 13704297 ns/op 16553 B/op 76 allocs/op +BenchmarkParseASCIITree/1000_Nodes 158 13422730 ns/op 16586 B/op 76 allocs/op +BenchmarkParseASCIITree/1000_Nodes-4 158 13677850 ns/op 16585 B/op 76 allocs/op +BenchmarkParseASCIITree/1000_Nodes-4 159 13631151 ns/op 16569 B/op 76 allocs/op +BenchmarkParseASCIITree/1000_Nodes-4 153 13676762 ns/op 16666 B/op 77 allocs/op +BenchmarkParseASCIITree/1000_Nodes-4 158 13912121 ns/op 16585 B/op 76 allocs/op +BenchmarkParseASCIITree/5000_Nodes 30 76028678 ns/op 54000 B/op 654 allocs/op +BenchmarkParseASCIITree/5000_Nodes 28 76955188 ns/op 56852 B/op 697 allocs/op +BenchmarkParseASCIITree/5000_Nodes 30 76314482 ns/op 54005 B/op 654 allocs/op +BenchmarkParseASCIITree/5000_Nodes 30 75581910 ns/op 53999 B/op 654 allocs/op +BenchmarkParseASCIITree/5000_Nodes-4 30 76865265 ns/op 54005 B/op 654 allocs/op +BenchmarkParseASCIITree/5000_Nodes-4 30 75691367 ns/op 54000 B/op 654 allocs/op +BenchmarkParseASCIITree/5000_Nodes-4 30 77527862 ns/op 54005 B/op 654 allocs/op +BenchmarkParseASCIITree/5000_Nodes-4 30 76256376 ns/op 53999 B/op 654 allocs/op +BenchmarkParseJSON/100_Nodes 619 3845768 ns/op 14233 B/op 46 allocs/op +BenchmarkParseJSON/100_Nodes 622 3839587 ns/op 14232 B/op 46 allocs/op +BenchmarkParseJSON/100_Nodes 626 3847376 ns/op 14232 B/op 46 allocs/op +BenchmarkParseJSON/100_Nodes 612 3856256 ns/op 14233 B/op 46 allocs/op +BenchmarkParseJSON/100_Nodes-4 603 3888006 ns/op 14235 B/op 46 allocs/op +BenchmarkParseJSON/100_Nodes-4 595 3830891 ns/op 14236 B/op 46 allocs/op +BenchmarkParseJSON/100_Nodes-4 619 3856311 ns/op 14233 B/op 46 allocs/op +BenchmarkParseJSON/100_Nodes-4 610 3862325 ns/op 14235 B/op 46 allocs/op +BenchmarkParseJSON/500_Nodes 259 8897651 ns/op 15021 B/op 56 allocs/op +BenchmarkParseJSON/500_Nodes 259 8823484 ns/op 15022 B/op 56 allocs/op +BenchmarkParseJSON/500_Nodes 255 8842256 ns/op 15035 B/op 56 allocs/op +BenchmarkParseJSON/500_Nodes 259 8839408 ns/op 15020 B/op 56 allocs/op +BenchmarkParseJSON/500_Nodes-4 258 9014751 ns/op 15026 B/op 56 allocs/op +BenchmarkParseJSON/500_Nodes-4 256 8910219 ns/op 15031 B/op 56 allocs/op +BenchmarkParseJSON/500_Nodes-4 250 8886906 ns/op 15052 B/op 56 allocs/op +BenchmarkParseJSON/500_Nodes-4 256 8924803 ns/op 15032 B/op 56 allocs/op +BenchmarkParseJSON/1000_Nodes 152 14157388 ns/op 16748 B/op 78 allocs/op +BenchmarkParseJSON/1000_Nodes 151 14274926 ns/op 16766 B/op 78 allocs/op +BenchmarkParseJSON/1000_Nodes 153 14178025 ns/op 16732 B/op 77 allocs/op +BenchmarkParseJSON/1000_Nodes 152 14402146 ns/op 16748 B/op 78 allocs/op +BenchmarkParseJSON/1000_Nodes-4 152 14529616 ns/op 16747 B/op 78 allocs/op +BenchmarkParseJSON/1000_Nodes-4 150 14358997 ns/op 16783 B/op 78 allocs/op +BenchmarkParseJSON/1000_Nodes-4 153 14577555 ns/op 16731 B/op 77 allocs/op +BenchmarkParseJSON/1000_Nodes-4 150 14707267 ns/op 16785 B/op 78 allocs/op +BenchmarkParseJSON/5000_Nodes 28 78470530 ns/op 56916 B/op 697 allocs/op +BenchmarkParseJSON/5000_Nodes 28 80320509 ns/op 56916 B/op 697 allocs/op +BenchmarkParseJSON/5000_Nodes 28 78820110 ns/op 56916 B/op 697 allocs/op +BenchmarkParseJSON/5000_Nodes 27 79474586 ns/op 58500 B/op 721 allocs/op +BenchmarkParseJSON/5000_Nodes-4 28 78235881 ns/op 56916 B/op 697 allocs/op +BenchmarkParseJSON/5000_Nodes-4 27 80390096 ns/op 58507 B/op 722 allocs/op +BenchmarkParseJSON/5000_Nodes-4 28 78790726 ns/op 56921 B/op 697 allocs/op +BenchmarkParseJSON/5000_Nodes-4 28 79593061 ns/op 56921 B/op 697 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes 259 8781598 ns/op 38524 B/op 46 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes 259 8750378 ns/op 38525 B/op 46 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes 262 8744519 ns/op 38513 B/op 45 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes 261 8827654 ns/op 38519 B/op 45 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes-4 261 8813442 ns/op 38519 B/op 45 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes-4 260 8783311 ns/op 38520 B/op 46 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes-4 249 8826489 ns/op 38561 B/op 46 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes-4 262 8776226 ns/op 38514 B/op 45 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes 262 8743410 ns/op 14090 B/op 45 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes 258 8796191 ns/op 14104 B/op 46 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes 262 8748289 ns/op 14091 B/op 45 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes 261 8755004 ns/op 14094 B/op 45 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes-4 256 8763959 ns/op 14111 B/op 46 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes-4 254 8810986 ns/op 14118 B/op 46 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes-4 256 8832127 ns/op 14110 B/op 46 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes-4 256 8800614 ns/op 14111 B/op 46 allocs/op PASS -ok github.com/jpwallace22/seed/benchmark 250.933s +ok github.com/jpwallace22/seed/benchmark 260.959s diff --git a/benchmark/historical/1_benchmark_results.txt b/benchmark/historical/1_benchmark_results.txt new file mode 100644 index 0000000..f6a7e57 --- /dev/null +++ b/benchmark/historical/1_benchmark_results.txt @@ -0,0 +1,66 @@ +goos: darwin +goarch: arm64 +pkg: github.com/jpwallace22/seed/benchmark +cpu: Apple M3 Pro +BenchmarkParseASCIITree/100_Nodes 279 8564668 ns/op 13872 B/op 47 allocs/op +BenchmarkParseASCIITree/100_Nodes 276 8583771 ns/op 13873 B/op 47 allocs/op +BenchmarkParseASCIITree/100_Nodes 276 8509018 ns/op 13869 B/op 47 allocs/op +BenchmarkParseASCIITree/100_Nodes-4 274 8841260 ns/op 13971 B/op 47 allocs/op +BenchmarkParseASCIITree/100_Nodes-4 274 8661431 ns/op 13887 B/op 47 allocs/op +BenchmarkParseASCIITree/100_Nodes-4 271 8579817 ns/op 13873 B/op 47 allocs/op +BenchmarkParseASCIITree/500_Nodes 61 35318035 ns/op 17105 B/op 87 allocs/op +BenchmarkParseASCIITree/500_Nodes 62 35815971 ns/op 17051 B/op 87 allocs/op +BenchmarkParseASCIITree/500_Nodes 60 35705947 ns/op 17163 B/op 88 allocs/op +BenchmarkParseASCIITree/500_Nodes-4 62 35331921 ns/op 17048 B/op 87 allocs/op +BenchmarkParseASCIITree/500_Nodes-4 61 35478822 ns/op 17106 B/op 87 allocs/op +BenchmarkParseASCIITree/500_Nodes-4 62 35632753 ns/op 17048 B/op 87 allocs/op +BenchmarkParseASCIITree/1000_Nodes 32 64032953 ns/op 25009 B/op 188 allocs/op +BenchmarkParseASCIITree/1000_Nodes 31 64733102 ns/op 25380 B/op 193 allocs/op +BenchmarkParseASCIITree/1000_Nodes 37 63564145 ns/op 23481 B/op 169 allocs/op +BenchmarkParseASCIITree/1000_Nodes-4 32 64366712 ns/op 25009 B/op 188 allocs/op +BenchmarkParseASCIITree/1000_Nodes-4 32 65188727 ns/op 25009 B/op 188 allocs/op +BenchmarkParseASCIITree/1000_Nodes-4 32 65007410 ns/op 25014 B/op 188 allocs/op +BenchmarkParseASCIITree/5000_Nodes 5 428583600 ns/op 235820 B/op 3482 allocs/op +BenchmarkParseASCIITree/5000_Nodes 5 431194442 ns/op 235820 B/op 3482 allocs/op +BenchmarkParseASCIITree/5000_Nodes 5 425311117 ns/op 235820 B/op 3482 allocs/op +BenchmarkParseASCIITree/5000_Nodes-4 5 423967017 ns/op 235856 B/op 3482 allocs/op +BenchmarkParseASCIITree/5000_Nodes-4 5 427780992 ns/op 235824 B/op 3482 allocs/op +BenchmarkParseASCIITree/5000_Nodes-4 5 432100433 ns/op 235824 B/op 3482 allocs/op +BenchmarkParseJSON/100_Nodes 266 8805398 ns/op 13941 B/op 47 allocs/op +BenchmarkParseJSON/100_Nodes 266 8795410 ns/op 13941 B/op 47 allocs/op +BenchmarkParseJSON/100_Nodes 266 8882962 ns/op 13940 B/op 47 allocs/op +BenchmarkParseJSON/100_Nodes-4 259 8968533 ns/op 13947 B/op 47 allocs/op +BenchmarkParseJSON/100_Nodes-4 264 8830172 ns/op 13942 B/op 47 allocs/op +BenchmarkParseJSON/100_Nodes-4 265 9740763 ns/op 13982 B/op 47 allocs/op +BenchmarkParseJSON/500_Nodes 60 38419985 ns/op 17227 B/op 88 allocs/op +BenchmarkParseJSON/500_Nodes 64 36303060 ns/op 17009 B/op 85 allocs/op +BenchmarkParseJSON/500_Nodes 62 36148765 ns/op 17112 B/op 87 allocs/op +BenchmarkParseJSON/500_Nodes-4 61 36262189 ns/op 17170 B/op 87 allocs/op +BenchmarkParseJSON/500_Nodes-4 61 36725622 ns/op 17173 B/op 87 allocs/op +BenchmarkParseJSON/500_Nodes-4 61 36688264 ns/op 17174 B/op 87 allocs/op +BenchmarkParseJSON/1000_Nodes 36 65342146 ns/op 23813 B/op 172 allocs/op +BenchmarkParseJSON/1000_Nodes 31 66417485 ns/op 25438 B/op 193 allocs/op +BenchmarkParseJSON/1000_Nodes 31 68197324 ns/op 25444 B/op 193 allocs/op +BenchmarkParseJSON/1000_Nodes-4 31 65345148 ns/op 25444 B/op 193 allocs/op +BenchmarkParseJSON/1000_Nodes-4 32 66411648 ns/op 25073 B/op 188 allocs/op +BenchmarkParseJSON/1000_Nodes-4 31 65663575 ns/op 25444 B/op 193 allocs/op +BenchmarkParseJSON/5000_Nodes 5 440192250 ns/op 235888 B/op 3482 allocs/op +BenchmarkParseJSON/5000_Nodes 5 435061700 ns/op 235920 B/op 3482 allocs/op +BenchmarkParseJSON/5000_Nodes 5 432037625 ns/op 235888 B/op 3482 allocs/op +BenchmarkParseJSON/5000_Nodes-4 5 430941733 ns/op 235888 B/op 3482 allocs/op +BenchmarkParseJSON/5000_Nodes-4 5 452172008 ns/op 235891 B/op 3482 allocs/op +BenchmarkParseJSON/5000_Nodes-4 5 439331125 ns/op 235891 B/op 3482 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes 61 35929608 ns/op 40638 B/op 77 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes 61 35659387 ns/op 40646 B/op 77 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes 61 36299533 ns/op 40644 B/op 77 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes-4 61 35834596 ns/op 40638 B/op 77 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes-4 62 35751481 ns/op 40583 B/op 77 allocs/op +BenchmarkInputMethods/StringInput_-_500_Nodes-4 62 35456389 ns/op 40583 B/op 77 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes 61 35676591 ns/op 16216 B/op 77 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes 62 35672579 ns/op 16163 B/op 77 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes 62 35439092 ns/op 16163 B/op 77 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes-4 62 35490837 ns/op 16163 B/op 77 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes-4 62 35664337 ns/op 16165 B/op 77 allocs/op +BenchmarkInputMethods/FileInput_-_500_nodes-4 62 35499835 ns/op 16163 B/op 77 allocs/op +PASS +ok github.com/jpwallace22/seed/benchmark 250.933s diff --git a/go.sum b/go.sum index 2782c60..d41aa08 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tiagomelo/go-clipboard v0.1.1 h1:nddQ5DsEnKW0KdzTILhbLpSq3e9y2dkJXEOtsMs6H7A= diff --git a/internal/filesystem/filesystem.go b/internal/filesystem/filesystem.go new file mode 100644 index 0000000..5008fef --- /dev/null +++ b/internal/filesystem/filesystem.go @@ -0,0 +1,125 @@ +package filesystem + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/jpwallace22/seed/pkg/logger" +) + +type Filesystem interface { + CreateFileSystem(*TreeNode, string, logger.Logger) error + CreateNode(name string, depth int) *TreeNode + CreateNodeWithType(name string, depth int, isFile bool) *TreeNode + CleanNodeTree(node *TreeNode) +} + +const permissions = os.FileMode(0755) + +var nodePool = sync.Pool{ + New: func() interface{} { + return &TreeNode{ + Children: make([]*TreeNode, 0, 4), + } + }, +} + +type TreeNode struct { + Name string + Children []*TreeNode + IsFile bool + Depth int +} + +type filesystem struct{} + +func New() Filesystem { + return &filesystem{} +} + +func (f *filesystem) CreateFileSystem(node *TreeNode, parentPath string, logger logger.Logger) error { + if node == nil { + return nil + } + + if node.Name == "." { + for _, child := range node.Children { + if err := f.CreateFileSystem(child, parentPath, logger); err != nil { + return err + } + } + return nil + } + + currentPath := parentPath + if len(parentPath) > 0 { + currentPath = filepath.Join(parentPath, node.Name) + } else { + currentPath = node.Name + } + + if node.IsFile { + if err := f.createFile(currentPath, logger); err != nil { + return err + } + } else { + if err := os.MkdirAll(currentPath, permissions); err != nil { + return fmt.Errorf("failed to create directory %s: %w", currentPath, err) + } + logger.Info("Planted directory: " + currentPath) + } + + for _, child := range node.Children { + if err := f.CreateFileSystem(child, currentPath, logger); err != nil { + return err + } + } + + return nil +} + +func (f *filesystem) createFile(path string, logger logger.Logger) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, permissions) + if err != nil { + if os.IsNotExist(err) { + if err := os.MkdirAll(filepath.Dir(path), permissions); err != nil { + return fmt.Errorf("failed to create parent directory for %s: %w", path, err) + } + file, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY, permissions) + } + if err != nil { + return fmt.Errorf("failed to create file %s: %w", path, err) + } + } + file.Close() + logger.Info("Planted file: " + path) + return nil +} + +func (f *filesystem) CreateNode(name string, depth int) *TreeNode { + return f.CreateNodeWithType(name, depth, strings.Contains(name, ".")) +} + +func (f *filesystem) CreateNodeWithType(name string, depth int, isFile bool) *TreeNode { + node := nodePool.Get().(*TreeNode) + node.Name = name + node.IsFile = isFile + node.Depth = depth + node.Children = node.Children[:0] + return node +} + +func (f *filesystem) CleanNodeTree(node *TreeNode) { + if node == nil { + return + } + + for _, child := range node.Children { + f.CleanNodeTree(child) + } + node.Children = node.Children[:0] + nodePool.Put(node) +} diff --git a/internal/parser/json_parser.go b/internal/parser/json_parser.go index 932c42a..6bf9cb9 100644 --- a/internal/parser/json_parser.go +++ b/internal/parser/json_parser.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/jpwallace22/seed/internal/ctx" + fs "github.com/jpwallace22/seed/internal/filesystem" ) type FileNode struct { @@ -21,10 +22,14 @@ type Report struct { type jsonParser struct { ctx *ctx.SeedContext + fs fs.Filesystem } func NewJSONParser(ctx *ctx.SeedContext) Parser { - return &jsonParser{ctx: ctx} + return &jsonParser{ + ctx: ctx, + fs: fs.New(), + } } func (p *jsonParser) ParseTree(jsonStr string) error { @@ -41,35 +46,31 @@ func (p *jsonParser) ParseTree(jsonStr string) error { return fmt.Errorf("empty JSON array") } - // First validate the root node structure - if err := p.validateNode(nodes[0]); err != nil { - return fmt.Errorf("failed to parse tree: %w", err) + var rootNode FileNode + if err := json.Unmarshal(nodes[0], &rootNode); err != nil { + return fmt.Errorf("failed to parse root node: %w", err) } - // Then parse into FileNode - var rootFileNode FileNode - if err := json.Unmarshal(nodes[0], &rootFileNode); err != nil { - return fmt.Errorf("failed to parse root node: %w", err) + if rootNode.Type == "" || rootNode.Name == "" { + return fmt.Errorf("missing required fields in root node") } - // Convert to TreeNode - rootTreeNode := p.fileNodeToTreeNode(&rootFileNode) + root := p.buildTreeFromJSON(&rootNode, 0) + defer p.fs.CleanNodeTree(root) - // Create filesystem using the existing function - if err := createFileSystem(rootTreeNode, "", p.ctx.Logger); err != nil { + if err := p.fs.CreateFileSystem(root, "", p.ctx.Logger); err != nil { return fmt.Errorf("failed to create filesystem: %w", err) } - // Verify report if present if len(nodes) > 1 { var report Report if err := json.Unmarshal(nodes[1], &report); err != nil { return fmt.Errorf("failed to parse report: %w", err) } - dirs, files := p.countTreeNodes(rootTreeNode) + dirs, files := p.countNodes(root) if dirs != report.Directories || files != report.Files { - return fmt.Errorf("file system count mismatch - expected: %d directories and %d files, got: %d directories and %d files", + return fmt.Errorf("count mismatch - expected: %d dirs, %d files, got: %d dirs, %d files", report.Directories, report.Files, dirs, files) } } @@ -77,66 +78,39 @@ func (p *jsonParser) ParseTree(jsonStr string) error { return nil } -func (p *jsonParser) validateNode(raw json.RawMessage) error { - var node struct { - Type string `json:"type"` - Name string `json:"name"` - Contents []json.RawMessage `json:"contents,omitempty"` - } - - if err := json.Unmarshal(raw, &node); err != nil { - return fmt.Errorf("invalid node: %w", err) - } - - if node.Type == "" { - return fmt.Errorf("missing type field") - } - - if node.Name == "" { - return fmt.Errorf("missing name field") - } +func (p *jsonParser) buildTreeFromJSON(node *FileNode, depth int) *fs.TreeNode { + treeNode := p.fs.CreateNodeWithType(node.Name, depth, node.Type == "file") - // Recursively validate contents - for _, content := range node.Contents { - if err := p.validateNode(content); err != nil { - return err + if len(node.Contents) > 0 { + treeNode.Children = make([]*fs.TreeNode, 0, len(node.Contents)) + for i := range node.Contents { + childNode := p.buildTreeFromJSON(&node.Contents[i], depth+1) + treeNode.Children = append(treeNode.Children, childNode) } } - return nil -} - -func (p *jsonParser) fileNodeToTreeNode(node *FileNode) *TreeNode { - treeNode := &TreeNode{ - name: node.Name, - isFile: node.Type == "file", - children: make([]*TreeNode, 0), - } - - for i := range node.Contents { - childNode := p.fileNodeToTreeNode(&node.Contents[i]) - treeNode.children = append(treeNode.children, childNode) - } - return treeNode } -func (p *jsonParser) countTreeNodes(node *TreeNode) (directories int, files int) { - if node == nil { +func (p *jsonParser) countNodes(root *fs.TreeNode) (directories int, files int) { + if root == nil { return 0, 0 } - if node.isFile { - files = 1 - } else { - directories = 1 - } + stack := make([]*fs.TreeNode, 0, 32) + stack = append(stack, root) - for _, child := range node.children { - d, f := p.countTreeNodes(child) - directories += d - files += f + for len(stack) > 0 { + node := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if node.IsFile { + files++ + } else { + directories++ + } + stack = append(stack, node.Children...) } - return directories, files + return } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 4ab5eac..cd916d5 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -2,25 +2,15 @@ package parser import ( "fmt" - "os" - "path/filepath" "github.com/jpwallace22/seed/cmd/flags" "github.com/jpwallace22/seed/internal/ctx" - "github.com/jpwallace22/seed/pkg/logger" ) type Parser interface { ParseTree(string) error } -type TreeNode struct { - name string - children []*TreeNode - isFile bool - depth int -} - type Option func(*config) type config struct { @@ -54,50 +44,3 @@ func WithFormat(format flags.Format) Option { c.format = format } } - -// This should move to a new module for filesystems, its breaking single job pattern -func createFileSystem(node *TreeNode, parentPath string, logger logger.Logger) error { - if node == nil { - return nil - } - permissions := os.FileMode(0755) - - currentPath := parentPath - if node.name != "." { - currentPath = filepath.Join(parentPath, node.name) - } - - // create current node unless it's the "." root - if node.name != "." { - if node.isFile { - // ensure parent directory exists - parentDir := filepath.Dir(currentPath) - if err := os.MkdirAll(parentDir, permissions); err != nil { - return fmt.Errorf("failed to create directory %s: %w", parentDir, err) - } - - // create file - f, err := os.Create(currentPath) - if err != nil { - return fmt.Errorf("failed to create file %s: %w", currentPath, err) - } - - f.Close() - logger.Info("Planted file: " + currentPath) - } else { - if err := os.MkdirAll(currentPath, permissions); err != nil { - return fmt.Errorf("failed to create directory %s: %w", currentPath, err) - } - logger.Info("Planted directory: " + currentPath) - } - } - - // loop through children with the correct parent path - for _, child := range node.children { - if err := createFileSystem(child, currentPath, logger); err != nil { - return err - } - } - - return nil -} diff --git a/internal/parser/tree_parser.go b/internal/parser/tree_parser.go index 3c889bf..c7033e9 100644 --- a/internal/parser/tree_parser.go +++ b/internal/parser/tree_parser.go @@ -5,44 +5,66 @@ import ( "strings" "github.com/jpwallace22/seed/internal/ctx" + fs "github.com/jpwallace22/seed/internal/filesystem" ) +const ( + verticalPipe = "│ " + fourSpaces = " " + teeConnector = "├── " + elConnector = "└── " +) + +var replacements = []struct { + old string + new string +}{ + {teeConnector, ""}, + {elConnector, ""}, + {verticalPipe, ""}, + {fourSpaces, ""}, + {"│", ""}, + {"└", ""}, + {"├", ""}, + {"─", ""}, + {"/", ""}, + {"\\", ""}, +} + type stringParser struct { - ctx *ctx.SeedContext + ctx *ctx.SeedContext + fs fs.Filesystem + replacer *strings.Replacer } func NewTreeParser(ctx *ctx.SeedContext) Parser { + pairs := make([]string, 0, len(replacements)*2) + for _, r := range replacements { + pairs = append(pairs, r.old, r.new) + } return &stringParser{ - ctx: ctx, + ctx: ctx, + fs: fs.New(), + replacer: strings.NewReplacer(pairs...), } } -// converts a text representation of a directory tree into actual directories and files func (p *stringParser) ParseTree(tree string) error { - lines := strings.Split(strings.TrimSpace(tree), "\n") - if len(lines) == 0 { + if len(strings.TrimSpace(tree)) == 0 { return fmt.Errorf("no tree provided") } - if strings.TrimSpace(lines[0]) == "tree" { - lines = lines[1:] - } - + lines := strings.Split(strings.TrimSpace(tree), "\n") root, err := p.buildTree(lines) if err != nil { return fmt.Errorf("failed to parse tree: %w", err) } + defer p.fs.CleanNodeTree(root) - err = createFileSystem(root, "", p.ctx.Logger) - if err != nil { - return err - } - - return nil + return p.fs.CreateFileSystem(root, "", p.ctx.Logger) } -// converts the string lines into a tree structure -func (p *stringParser) buildTree(lines []string) (*TreeNode, error) { +func (p *stringParser) buildTree(lines []string) (*fs.TreeNode, error) { if len(lines) == 0 { return nil, fmt.Errorf("no lines to parse") } @@ -52,84 +74,71 @@ func (p *stringParser) buildTree(lines []string) (*TreeNode, error) { return nil, fmt.Errorf("a root is required") } - root := &TreeNode{ - name: rootName, - isFile: strings.Contains(rootName, ".") && rootName != ".", - children: make([]*TreeNode, 0), - depth: 0, - } + root := p.fs.CreateNode(rootName, 0) + lastNodes := map[int]*fs.TreeNode{0: root} - // Keep track of last nodes at each depth level - lastNodes := make(map[int]*TreeNode) - lastNodes[0] = root + if len(lines) > 1 { + root.Children = make([]*fs.TreeNode, 0, len(lines)/4) + } for i := 1; i < len(lines); i++ { - // Need to normalize the line by changing all spaces with ASCII - line := strings.ReplaceAll(lines[i], "\u00a0", " ") - - if strings.TrimSpace(line) == "" { - continue + if err := p.processLine(lines[i], lastNodes); err != nil { + return nil, err } + } - // Build the node - depth := p.getDepth(line) - name := p.extractName(line) - if name == "" { - continue - } + return root, nil +} - node := &TreeNode{ - name: name, - isFile: strings.Contains(name, "."), - children: make([]*TreeNode, 0), - depth: depth, - } +func (p *stringParser) processLine(line string, lastNodes map[int]*fs.TreeNode) error { + line = strings.ReplaceAll(line, "\u00a0", " ") + trimmed := strings.TrimSpace(line) + if trimmed == "" { + return nil + } - // Assign the node to a parent - parentDepth := depth - 1 - parent := lastNodes[parentDepth] - if parent == nil { - return nil, fmt.Errorf("invalid tree structure: missing parent at depth %d for node %s", parentDepth, name) - } + depth := p.getDepth(line) + name := strings.TrimSpace(p.replacer.Replace(trimmed)) + if name == "" { + return nil + } - parent.children = append(parent.children, node) - lastNodes[depth] = node + node := p.fs.CreateNode(name, depth) + parentDepth := depth - 1 + if depth == 0 { + parentDepth = 0 } - return root, nil + parent := lastNodes[parentDepth] + if parent == nil { + return fmt.Errorf("invalid tree structure: missing parent at depth %d for node %s", parentDepth, name) + } + + parent.Children = append(parent.Children, node) + lastNodes[depth] = node + return nil } func (p *stringParser) getDepth(line string) int { depth := 0 - for i := 0; i < len(line); { - if strings.HasPrefix(line[i:], "│ ") { - depth++ - i += 4 - } else if strings.HasPrefix(line[i:], " ") { - depth++ - i += 4 - } else if strings.HasPrefix(line[i:], "├── ") { - depth++ - i += 4 - } else if strings.HasPrefix(line[i:], "└── ") { + i := 0 + + for i < len(line) { + switch { + case strings.HasPrefix(line[i:], verticalPipe), + strings.HasPrefix(line[i:], fourSpaces): depth++ i += 4 - } else { + case strings.HasPrefix(line[i:], teeConnector), + strings.HasPrefix(line[i:], elConnector): + return depth + 1 + default: + if i == 0 { + return 0 + } i++ } } - return depth -} -func (p *stringParser) extractName(line string) string { - line = strings.TrimSpace(line) - unwantedChars := []string{ - "├── ", "└── ", "/", "\\", - "│ ", " ", - "│", "└", "├", "─", - } - for _, char := range unwantedChars { - line = strings.ReplaceAll(line, char, "") - } - return strings.TrimSpace(line) + return depth } diff --git a/internal/parser/tree_parser_test.go b/internal/parser/tree_parser_test.go index ba66c09..bee2aa0 100644 --- a/internal/parser/tree_parser_test.go +++ b/internal/parser/tree_parser_test.go @@ -47,24 +47,6 @@ func (s *ParserTestSuite) TestEmptyInput() { }) } -func (s *ParserTestSuite) TestTreePrefix() { - input := `tree -root -├── file1.txt -└── file2.txt` - - expectedFiles := []string{ - "root/file1.txt", - "root/file2.txt", - } - expectedDirs := []string{"root"} - - s.Run("create tree with prefix", func() { - s.Require().NoError(s.parser.ParseTree(input)) - s.verifyStructure(expectedFiles, expectedDirs) - }) -} - func (s *ParserTestSuite) TestSimpleStructure() { input := `root ├── dir1 diff --git a/tools/scripts/archive-benchmark.sh b/tools/scripts/archive-benchmark.sh new file mode 100644 index 0000000..c76c319 --- /dev/null +++ b/tools/scripts/archive-benchmark.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Add debug mode +if [[ "${TRACE-0}" == "1" ]]; then + set -x +fi + +# Archives benchmark results with an incremented number +archive_benchmark() { + local current_file="./benchmark/benchmark_results.txt" + local historical_dir="./benchmark/historical" + + echo "Checking for current file: $current_file" + if [ ! -f "$current_file" ]; then + echo "No benchmark results file found at $current_file" + return 0 # This might be the issue - returning 0 when file doesn't exist + fi + + # Create historical directory if it doesn't exist + echo "Checking historical directory: $historical_dir" + if [ ! -d "$historical_dir" ]; then + echo "Creating historical directory at $historical_dir" + mkdir -p "$historical_dir" + fi + + # Find the next number - add error checking + echo "Finding next number..." + next_num=$(ls -1 "$historical_dir" 2>/dev/null | grep -E '^[0-9]+_benchmark_results\.txt$' | sed 's/_.*$//' | sort -n | tail -1 || echo "0") + + if [ -z "$next_num" ]; then + next_num=0 + echo "No existing benchmark archives found, starting at 1" + else + echo "Last benchmark archive number: $next_num" + fi + next_num=$((next_num + 1)) + + # Archive the file + local archive_file="$historical_dir/${next_num}_benchmark_results.txt" + mv "$current_file" "$archive_file" || { + echo "Move failed with error code $?" + return 1 + } + + if [ -f "$archive_file" ]; then + echo "Successfully archived benchmark results to $archive_file" + return 0 + else + echo "ERROR: Failed to archive benchmark results" + return 1 + fi +} + +# Run the archiving function +archive_benchmark || { + echo "Archive function failed with error code $?" + exit 1 +} diff --git a/tools/scripts/compare-benchmarks.sh b/tools/scripts/compare-benchmarks.sh new file mode 100755 index 0000000..e9d2beb --- /dev/null +++ b/tools/scripts/compare-benchmarks.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${TRACE-0}" == "1" ]]; then + set -x +fi + +historical_dir="./benchmark/historical" +current_file="./benchmark/benchmark_results.txt" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}Enhanced Benchmark Analysis Tool${NC}" +echo "--------------------------------" + +if [ ! -f "$current_file" ]; then + echo -e "${RED}Error: No current benchmark file found at $current_file${NC}" + exit 1 +fi + +latest_historical=$(ls -1 "$historical_dir" | grep -E '^[0-9]+_benchmark_results\.txt$' | sort -n | tail -1) +if [ -z "$latest_historical" ]; then + echo -e "${RED}Error: No historical benchmark file found to compare against${NC}" + exit 1 +fi + +echo -e "${YELLOW}Analyzing Benchmarks:${NC}" +echo "Old: $historical_dir/$latest_historical" +echo "New: $current_file" +echo "--------------------------------" + +# Function to extract values for a specific benchmark +extract_value() { + local file=$1 + local benchmark=$2 + local metric=$3 + grep "$benchmark" "$file" | grep "$metric" | head -n 1 | awk '{print $3}' +} + +# Function to calculate percentage change +calc_percentage_change() { + local old=$1 + local new=$2 + echo "scale=2; (($new - $old) / $old) * 100" | bc +} + +# Function to convert B/op to human readable format +format_bytes() { + local bytes=$1 + if [ $bytes -gt 1048576 ]; then + echo "scale=2; $bytes/1048576" | bc + echo "MB" + elif [ $bytes -gt 1024 ]; then + echo "scale=2; $bytes/1024" | bc + echo "KB" + else + echo "$bytes" + echo "B" + fi +} + +# Key benchmarks to analyze +declare -a benchmarks=( + "ParseASCIITree/100_Nodes" + "ParseASCIITree/1000_Nodes" + "ParseASCIITree/5000_Nodes" + "ParseJSON/100_Nodes" + "ParseJSON/1000_Nodes" + "ParseJSON/5000_Nodes" +) + +echo -e "\n${BLUE}Detailed Performance Analysis:${NC}" +echo "----------------------------------------" + +for benchmark in "${benchmarks[@]}"; do + echo -e "\n${YELLOW}$benchmark:${NC}" + + # Extract values + old_time=$(extract_value "$historical_dir/$latest_historical" "$benchmark" "ns/op") + new_time=$(extract_value "$current_file" "$benchmark" "ns/op") + old_allocs=$(extract_value "$historical_dir/$latest_historical" "$benchmark" "allocs/op") + new_allocs=$(extract_value "$current_file" "$benchmark" "allocs/op") + old_bytes=$(extract_value "$historical_dir/$latest_historical" "$benchmark" "B/op") + new_bytes=$(extract_value "$current_file" "$benchmark" "B/op") + + # Convert nanoseconds to milliseconds for readability + old_ms=$(echo "scale=2; $old_time / 1000000" | bc) + new_ms=$(echo "scale=2; $new_time / 1000000" | bc) + + # Calculate changes + time_change=$(calc_percentage_change $old_ms $new_ms) + mem_change=$(calc_percentage_change $old_bytes $new_bytes) + alloc_change=$(calc_percentage_change $old_allocs $new_allocs) + + # Format memory values + read old_mem_val old_mem_unit <<<$(format_bytes $old_bytes) + read new_mem_val new_mem_unit <<<$(format_bytes $new_bytes) + + # Determine if it's an improvement + if (($(echo "$time_change < 0" | bc -l))); then + time_color=$GREEN + time_direction="faster" + time_change=${time_change#-} + else + time_color=$RED + time_direction="slower" + fi + + if (($(echo "$mem_change < 0" | bc -l))); then + mem_color=$GREEN + mem_direction="less" + mem_change=${mem_change#-} + else + mem_color=$RED + mem_direction="more" + fi + + if (($(echo "$alloc_change < 0" | bc -l))); then + alloc_color=$GREEN + alloc_direction="fewer" + alloc_change=${alloc_change#-} + else + alloc_color=$RED + alloc_direction="more" + fi + + echo -e "Time: ${old_ms}ms → ${new_ms}ms" + echo -e "Performance: ${time_color}${time_change}% ${time_direction}${NC}" + echo -e "Memory: ${old_mem_val}${old_mem_unit} → ${new_mem_val}${new_mem_unit}" + echo -e "Memory Usage: ${mem_color}${mem_change}% ${mem_direction}${NC}" + echo -e "Allocations: ${old_allocs} → ${new_allocs}" + echo -e "Allocation Change: ${alloc_color}${alloc_change}% ${alloc_direction}${NC}" +done + +echo -e "\n${YELLOW}Summary of Major Changes:${NC}" +echo "----------------------------------------" +# Summary for 5000 nodes benchmarks +old_large_time=$(extract_value "$historical_dir/$latest_historical" "5000_Nodes" "ns/op" | head -n 1) +new_large_time=$(extract_value "$current_file" "5000_Nodes" "ns/op" | head -n 1) +old_large_mem=$(extract_value "$historical_dir/$latest_historical" "5000_Nodes" "B/op" | head -n 1) +new_large_mem=$(extract_value "$current_file" "5000_Nodes" "B/op" | head -n 1) + +time_change=$(calc_percentage_change $old_large_time $new_large_time) +mem_change=$(calc_percentage_change $old_large_mem $new_large_mem) + +read old_mem_val old_mem_unit <<<$(format_bytes $old_large_mem) +read new_mem_val new_mem_unit <<<$(format_bytes $new_large_mem) + +echo -e "Large Operations (5000 nodes):" +if (($(echo "$time_change < 0" | bc -l))); then + echo -e "${GREEN}Performance: ${time_change#-}% faster${NC}" +else + echo -e "${RED}Performance: ${time_change}% slower${NC}" +fi + +if (($(echo "$mem_change < 0" | bc -l))); then + echo -e "${GREEN}Memory: ${mem_change#-}% less (${old_mem_val}${old_mem_unit} → ${new_mem_val}${new_mem_unit})${NC}" +else + echo -e "${RED}Memory: ${mem_change}% more (${old_mem_val}${old_mem_unit} → ${new_mem_val}${new_mem_unit})${NC}" +fi + +echo -e "\n${BLUE}Notes:${NC}" +echo "* Percentages show relative change from old to new" +echo "* Negative percentages indicate improvements" +echo "* Performance is measured in milliseconds for readability" +echo "* Memory is shown in appropriate units (B, KB, MB)" +echo "* Use TRACE=1 for debug output"