pgn2 0.4.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +50 -0
  3. data/.github/workflows/publish.yml +75 -0
  4. data/.github/workflows/release.yml +103 -0
  5. data/.gitignore +2 -1
  6. data/.rubocop.yml +38 -0
  7. data/CHANGELOG.md +84 -0
  8. data/README.md +122 -4
  9. data/Rakefile +20 -0
  10. data/TODO.md +9 -0
  11. data/bench/.keep +0 -0
  12. data/bench/IMPROVEMENTS.md +143 -0
  13. data/bench/baseline_moves.pre-optimization.txt +22 -0
  14. data/bench/baseline_moves.pre-quickwins.txt +23 -0
  15. data/bench/baseline_moves.txt +22 -0
  16. data/bench/baseline_parse.pre-optimization.txt +25 -0
  17. data/bench/baseline_parse.pre-quickwins.txt +26 -0
  18. data/bench/baseline_parse.racc.txt +25 -0
  19. data/bench/baseline_parse.txt +25 -0
  20. data/bench/profile_moves.rb +53 -0
  21. data/bench/profile_parse.rb +44 -0
  22. data/docs/superpowers/plans/2026-08-12-efficiency-optimizations.md +573 -0
  23. data/docs/superpowers/plans/2026-08-12-efficiency-tests-and-profiling.md +1091 -0
  24. data/docs/superpowers/plans/2026-08-12-to-pgn-serialization.md +162 -0
  25. data/docs/superpowers/plans/2026-08-13-whittle-to-racc-migration.md +130 -0
  26. data/docs/superpowers/specs/2026-08-12-to-pgn-serialization-design.md +217 -0
  27. data/docs/superpowers/specs/2026-08-13-pgn-performance-quick-wins-design.md +227 -0
  28. data/lib/pgn/board.rb +33 -15
  29. data/lib/pgn/fen.rb +16 -8
  30. data/lib/pgn/game.rb +23 -3
  31. data/lib/pgn/lexer.rb +223 -0
  32. data/lib/pgn/move.rb +12 -5
  33. data/lib/pgn/move_calculator.rb +27 -21
  34. data/lib/pgn/parser.rb +13 -203
  35. data/lib/pgn/pgn_parser.rb +393 -0
  36. data/lib/pgn/pgn_parser.y +140 -0
  37. data/lib/pgn/position.rb +3 -2
  38. data/lib/pgn/serializer.rb +141 -0
  39. data/lib/pgn/version.rb +1 -1
  40. data/lib/pgn.rb +3 -0
  41. data/pgn2.gemspec +12 -2
  42. data/spec/board_spec.rb +111 -0
  43. data/spec/fen_spec.rb +25 -0
  44. data/spec/game_spec.rb +74 -0
  45. data/spec/lexer_spec.rb +153 -0
  46. data/spec/move_calculator_spec.rb +226 -0
  47. data/spec/move_spec.rb +136 -0
  48. data/spec/parser_explicit_spec.rb +210 -0
  49. data/spec/parser_spec.rb +6 -23
  50. data/spec/pgn_files/doublequotes.pgn +21 -0
  51. data/spec/pgn_files/specialcharacters.pgn +79 -0
  52. data/spec/position_spec.rb +73 -0
  53. data/spec/serializer_spec.rb +89 -0
  54. data/spec/spec_helper.rb +0 -1
  55. metadata +103 -15
@@ -0,0 +1,143 @@
1
+ # Efficiency improvements — before/after
2
+
3
+ Captured by `rake bench` on the same machine. "BEFORE" = `bench/*.pre-optimization.txt`
4
+ (pre-optimization snapshot). "AFTER" = `bench/baseline_*.txt`.
5
+
6
+ ## bench/profile_moves.rb (immortal game, 45 plies)
7
+
8
+ | Metric | BEFORE | AFTER | Δ |
9
+ |---|---|---|---|
10
+ | Replay allocations (objects) | 5124 | 2565 | -2559 |
11
+ | Replay allocations (bytes) | 262608 | 155296 | -107312 |
12
+ | Board#dup x45 (objects) | 451 | 91 | -360 |
13
+ | Board#dup x45 (bytes) | 43096 | 6736 | -36360 |
14
+ | Board#at(str) x1000 (objects) | 6000 | 0 | -6000 |
15
+ | Board#at(str) x1000 (bytes) | 240000 | 0 | -240000 |
16
+
17
+ ## bench/profile_parse.rb (500 immortal games)
18
+
19
+ | Metric | BEFORE | AFTER | Δ |
20
+ |---|---|---|---|
21
+ | Parse-only allocations (objects) | 1248065 | 1248065 | 0 |
22
+ | Parse-only allocations (bytes) | 120370470 | 120370470 | 0 |
23
+ | Parse + replay allocations (objects) | 3778073 | 2498573 | -1279500 |
24
+ | Parse + replay allocations (bytes) | 249570048 | 195914048 | -53656000 |
25
+
26
+ ## Changes applied
27
+
28
+ 1. `Board#at(str)` / `coordinates_for` — getbyte arithmetic (zero-alloc string lookup).
29
+ 2. `MoveCalculator#king_position` — early exit.
30
+ 3. `Move#initialize` — explicit setters (no per-move `names` array).
31
+ 4. `FEN#board_string` — single-pass serialization.
32
+ 5. `Board` — column-level copy-on-write (`dup` shares columns, `update` clones one).
33
+
34
+ All existing characterization specs remain green; public output (FEN, PGN) is byte-identical.
35
+
36
+ ## Parser migration: whittle -> Racc + StringScanner (2026-08-13)
37
+
38
+ The abandoned `whittle` gem (v0.0.8, 2011) was replaced by a stdlib `Racc` +
39
+ `StringScanner` parser (`lib/pgn/pgn_parser.y`, generated to
40
+ `lib/pgn/pgn_parser.rb`; lexer in `lib/pgn/lexer.rb`). whittle was responsible
41
+ for ~80% of parse allocations.
42
+
43
+ Corpus: 500 immortal games (`BENCH_N=500`). Baseline = `bench/baseline_parse.txt`
44
+ (whittle). New = `bench/baseline_parse.racc.txt`. Allocation counts are
45
+ deterministic; throughput is noisy over a 5 s window (use ms/i).
46
+
47
+ | Metric | whittle | racc | Δ |
48
+ |---|---|---|---|
49
+ | Parse-only allocations (objects) | 1248065 | 557035 | -691030 (-55.4%) |
50
+ | Parse-only allocations (bytes) | 120370470 | 36636902 | -83733568 (-69.6%) |
51
+ | Parse + replay allocations (objects) | 2498573 | 1614087 | -884486 (-35.4%) |
52
+ | Parse + replay allocations (bytes) | 195914048 | 105404152 | -90509896 (-46.2%) |
53
+ | Parse-only throughput (ms/i) | 741 | 212 | -529 (~3.5x faster) |
54
+ | Parse + replay throughput (ms/i) | 1114 | 371 | -743 (~3.0x faster) |
55
+
56
+ ### What changed
57
+ - `lib/pgn/pgn_parser.y` / `pgn_parser.rb`: Racc grammar mirroring the whittle
58
+ rules, held as instance state (fixes the `@@pgn`/`@@game_comment` reentrancy
59
+ bug). `PGN::Game#pgn` is sliced from per-game byte offsets (no O(n^2)
60
+ `@@pgn +=` accumulation).
61
+ - `lib/pgn/lexer.rb`: StringScanner (C ext) lexer reusing whittle's exact
62
+ terminal regexes; records per-game content-start byte offsets.
63
+ - `lib/pgn/parser.rb`: thin facade delegating to `PGN::PgnParser`.
64
+ - whittle dependency dropped; `lib/pgn/whittle_parser.rb` deleted.
65
+
66
+ ### Behavior preservation
67
+ The grammar deliberately replicates whittle's quirks so parsed-game
68
+ serialization stays byte-compatible: right-recursive `variation_list` (variation
69
+ order reverses) and right-recursive `tag_section` (reverse insertion order,
70
+ first-wins). `game.pgn` is verbatim raw text. A golden-equivalence spec (now
71
+ removed with whittle) confirmed identical output on all 14 fixtures + 11 inline
72
+ inputs during the migration; 32 explicit parser specs now pin the behavior
73
+ permanently (`spec/parser_explicit_spec.rb`).
74
+
75
+ Full suite: 187 examples, 0 failures.
76
+
77
+ ## Quick wins (Approach A) — 2026-08-13
78
+
79
+ Safe, behavior-compatible micro-optimizations on top of the Racc parser.
80
+ Spec: `docs/superpowers/specs/2026-08-13-pgn-performance-quick-wins-design.md`.
81
+ "BEFORE" = `bench/baseline_*.pre-quickwins.txt` (working tree immediately before
82
+ this change). "AFTER" = `bench/baseline_*.txt`.
83
+
84
+ ### bench/profile_moves.rb (immortal game, 45 plies)
85
+
86
+ | Metric | BEFORE | AFTER | Δ |
87
+ |---|---|---|---|
88
+ | Replay allocations (objects) | 2177 | 1710 | -467 (-21.4%) |
89
+ | Replay allocations (bytes) | 141616 | 103760 | -37856 (-26.7%) |
90
+ | Replay throughput (µs/i) | 931.70 | 848.64 | -83.06 (-8.9%) |
91
+
92
+ ### bench/profile_parse.rb (500 immortal games)
93
+
94
+ | Metric | BEFORE | AFTER | Δ |
95
+ |---|---|---|---|
96
+ | Parse-only allocations (objects) | 626037 | 603537 | -22500 (-3.6%) |
97
+ | Parse-only allocations (bytes) | 39417414 | 28257414 | -11160000 (-28.3%) |
98
+ | Parse-only throughput (ms/i) | 318.39 | 274.08 | -44.31 (-13.9%) |
99
+ | Parse + replay allocations (objects) | 1683087 | 1427586 | -255501 (-15.2%) |
100
+ | Parse + replay allocations (bytes) | 108184152 | 78104136 | -30080016 (-27.8%) |
101
+ | Parse + replay throughput (ms/i) | 795.32 | 715.05 | -80.27 (-10.1%) |
102
+
103
+ ### Changes applied
104
+
105
+ 1. `PGN::Lexer` — added `next_token_pair` (returns `[type, value]`, no `Token`
106
+ Struct) built on a shared private `scan_next` routine that preserves the
107
+ `note_token`/`advance_line`/`game_starts` side effects. `next_token`/`tokens`
108
+ unchanged. `PgnParser#next_token` (in `.y` and generated `.rb`) now uses
109
+ `next_token_pair`. Eliminates the per-token `Token` Struct + its
110
+ `keyword_init` Hash (the larger win in bytes).
111
+ 2. `PGN::Game#moves=` — reuses an existing `MoveText` directly when its comment
112
+ is already fully cleaned (nil or brace-free); still re-wraps (preserving the
113
+ legacy double-`clean_text` for multi-line/nested comments) when the comment
114
+ carries braces. Halves `MoveText` allocations on the parse path for
115
+ comment-free corpora.
116
+ 3. `PGN::Move#piece=` — replaced the per-`Move.new` `san.match('O-O')` guard
117
+ (allocated a `MatchData` on every move, castling or not) with a
118
+ non-allocating `san.start_with?('O')`. The full hand-rolled SAN parser was
119
+ **deferred** per the spec's "measure first; defer if marginal" guidance:
120
+ it would save only ~1 `MatchData`/ply (~2% of replay, ~1.3% of parse+replay)
121
+ at high risk to SAN edge cases.
122
+ 4. `PGN::Position#next_player` — `(PLAYERS - [player]).first` →
123
+ `player == :white ? :black : :white` (removes 2 array allocations/ply).
124
+ `Position#move` — skips `castling - restrictions` when `restrictions` is
125
+ empty (returns the shared `castling` array; safe because castling arrays
126
+ are replaced, never mutated).
127
+ `PGN::MoveCalculator` — memoizes `destination_coords` (was recomputed 2–3
128
+ times/move, each allocating a 2-element array); frozen `ROOK_RESTRICTIONS`
129
+ constant replaces per-call hash literals in `castling_restrictions`; empty
130
+ short-circuit avoids `compact.uniq` on the common empty path.
131
+ `PGN::Move#pawn?` — `%w[P p].include?` → `piece == 'P' || piece == 'p'`.
132
+ `valid_square?` was left unchanged: its `(0..7)` are frozen range literals
133
+ cached by the VM, so inlining would only save method dispatch, not
134
+ allocations (the original rationale was unfounded).
135
+
136
+ ### Behavior preservation
137
+
138
+ All 182 specs pass unmodified (`bundle exec rspec`). The lexer refactor keeps
139
+ `next_token`/`tokens` and the `game_starts`-driven verbatim `Game#pgn` slicing
140
+ byte-identical (covered by `spec/lexer_spec.rb`, `spec/parser_explicit_spec.rb`,
141
+ and the fixture round-trip in `spec/game_spec.rb`). Racc parser is in sync with
142
+ `pgn_parser.y` (CI racc-sync check passes). No public API or serialized-output
143
+ changes.
@@ -0,0 +1,22 @@
1
+ Workload: immortal game, 45 plies
2
+
3
+ === 1. Replay allocations (45 plies, no parse) ===
4
+ total_allocated objects: 5124
5
+ total_allocated bytes: 262608
6
+
7
+ === 2. Board#dup x45 (target of flat-board COW) ===
8
+ total_allocated objects: 451
9
+ total_allocated bytes: 43096
10
+
11
+ === 3. Board#at(str) x1000 (target of coord-arithmetic at) ===
12
+ total_allocated objects: 6000
13
+ total_allocated bytes: 240000
14
+
15
+ === 4. Replay throughput (ips, excluding parse) ===
16
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
17
+ Warming up --------------------------------------
18
+ replay immortal 75.000 i/100ms
19
+ Calculating -------------------------------------
20
+ replay immortal 1.132k (±46.9%) i/s (883.58 μs/i) - 5.700k in 5.036401s
21
+
22
+ Done. Compare this file against bench/baseline_moves.txt after optimizations.
@@ -0,0 +1,23 @@
1
+ Workload: immortal game, 45 plies
2
+
3
+ === 1. Replay allocations (45 plies, no parse) ===
4
+ total_allocated objects: 2177
5
+ total_allocated bytes: 141616
6
+
7
+ === 2. Board#dup x45 (target of flat-board COW) ===
8
+ total_allocated objects: 136
9
+ total_allocated bytes: 10336
10
+
11
+ === 3. Board#at(str) x1000 (target of coord-arithmetic at) ===
12
+ total_allocated objects: 0
13
+ total_allocated bytes: 0
14
+
15
+ === 4. Replay throughput (ips, excluding parse) ===
16
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
17
+ Warming up --------------------------------------
18
+ replay immortal 83.000 i/100ms
19
+ Calculating -------------------------------------
20
+ replay immortal 1.073k (± 3.2%) i/s (931.70 μs/i) - 5.395k in 5.026523s
21
+
22
+ Done. Snapshot of the working tree immediately before the quick-wins (Approach A) changes.
23
+ Compare bench/baseline_moves.txt (after) against this file.
@@ -0,0 +1,22 @@
1
+ Workload: immortal game, 45 plies
2
+
3
+ === 1. Replay allocations (45 plies, no parse) ===
4
+ total_allocated objects: 1710
5
+ total_allocated bytes: 103760
6
+
7
+ === 2. Board#dup x45 (target of flat-board COW) ===
8
+ total_allocated objects: 136
9
+ total_allocated bytes: 10336
10
+
11
+ === 3. Board#at(str) x1000 (target of coord-arithmetic at) ===
12
+ total_allocated objects: 0
13
+ total_allocated bytes: 0
14
+
15
+ === 4. Replay throughput (ips, excluding parse) ===
16
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
17
+ Warming up --------------------------------------
18
+ replay immortal 112.000 i/100ms
19
+ Calculating -------------------------------------
20
+ replay immortal 1.178k (± 4.6%) i/s (848.64 μs/i) - 5.936k in 5.037516s
21
+
22
+ Done. Compare this file against bench/baseline_moves.txt after optimizations.
@@ -0,0 +1,25 @@
1
+ Corpus: 500 copies of the immortal game
2
+
3
+ === 1. Parse-only allocations (500 games) ===
4
+ total_allocated objects: 1248065
5
+ total_allocated bytes: 120370470
6
+
7
+ === 2. Parse + replay allocations (500 games) ===
8
+ total_allocated objects: 3778073
9
+ total_allocated bytes: 249570048
10
+
11
+ === 3. Parse-only throughput (ips) ===
12
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
13
+ Warming up --------------------------------------
14
+ parse 500 games 1.000 i/100ms
15
+ Calculating -------------------------------------
16
+ parse 500 games 1.513 (± 0.0%) i/s (661.01 ms/i) - 8.000 in 5.288062s
17
+
18
+ === 4. Parse + replay throughput (ips) ===
19
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
20
+ Warming up --------------------------------------
21
+ parse+replay 500 games 1.000 i/100ms
22
+ Calculating -------------------------------------
23
+ parse+replay 500 games 0.933 (± 0.0%) i/s (1.07 s/i) - 6.000 in 6.431545s
24
+
25
+ Done. Compare this file against bench/baseline_parse.txt after optimizations.
@@ -0,0 +1,26 @@
1
+ Corpus: 500 copies of the immortal game
2
+
3
+ === 1. Parse-only allocations (500 games) ===
4
+ total_allocated objects: 626037
5
+ total_allocated bytes: 39417414
6
+
7
+ === 2. Parse + replay allocations (500 games) ===
8
+ total_allocated objects: 1683087
9
+ total_allocated bytes: 108184152
10
+
11
+ === 3. Parse-only throughput (ips) ===
12
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
13
+ Warming up --------------------------------------
14
+ parse 500 games 1.000 i/100ms
15
+ Calculating -------------------------------------
16
+ parse 500 games 3.141 (± 0.0%) i/s (318.39 ms/i) - 16.000 in 5.094185s
17
+
18
+ === 4. Parse + replay throughput (ips) ===
19
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
20
+ Warming up --------------------------------------
21
+ parse+replay 500 games 1.000 i/100ms
22
+ Calculating -------------------------------------
23
+ parse+replay 500 games 1.257 (± 0.0%) i/s (795.32 ms/i) - 7.000 in 5.567259s
24
+
25
+ Done. Snapshot of the working tree immediately before the quick-wins (Approach A) changes.
26
+ Compare bench/baseline_parse.txt (after) against this file.
@@ -0,0 +1,25 @@
1
+ Corpus: 500 copies of the immortal game
2
+
3
+ === 1. Parse-only allocations (500 games) ===
4
+ total_allocated objects: 557035
5
+ total_allocated bytes: 36636902
6
+
7
+ === 2. Parse + replay allocations (500 games) ===
8
+ total_allocated objects: 1614087
9
+ total_allocated bytes: 105404152
10
+
11
+ === 3. Parse-only throughput (ips) ===
12
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
13
+ Warming up --------------------------------------
14
+ parse 500 games 1.000 i/100ms
15
+ Calculating -------------------------------------
16
+ parse 500 games 4.711 (±63.7%) i/s (212.27 ms/i) - 24.000 in 5.094454s
17
+
18
+ === 4. Parse + replay throughput (ips) ===
19
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
20
+ Warming up --------------------------------------
21
+ parse+replay 500 games 1.000 i/100ms
22
+ Calculating -------------------------------------
23
+ parse+replay 500 games 2.698 (±37.1%) i/s (370.63 ms/i) - 14.000 in 5.188813s
24
+
25
+ Done. Compare this file against bench/baseline_parse.txt after optimizations.
@@ -0,0 +1,25 @@
1
+ Corpus: 500 copies of the immortal game
2
+
3
+ === 1. Parse-only allocations (500 games) ===
4
+ total_allocated objects: 603537
5
+ total_allocated bytes: 28257414
6
+
7
+ === 2. Parse + replay allocations (500 games) ===
8
+ total_allocated objects: 1427586
9
+ total_allocated bytes: 78104136
10
+
11
+ === 3. Parse-only throughput (ips) ===
12
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
13
+ Warming up --------------------------------------
14
+ parse 500 games 1.000 i/100ms
15
+ Calculating -------------------------------------
16
+ parse 500 games 3.649 (±27.4%) i/s (274.08 ms/i) - 19.000 in 5.207546s
17
+
18
+ === 4. Parse + replay throughput (ips) ===
19
+ ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
20
+ Warming up --------------------------------------
21
+ parse+replay 500 games 1.000 i/100ms
22
+ Calculating -------------------------------------
23
+ parse+replay 500 games 1.399 (± 0.0%) i/s (715.05 ms/i) - 7.000 in 5.005330s
24
+
25
+ Done. Compare this file against bench/baseline_parse.txt after optimizations.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ # Measures per-move allocation and replay throughput for the move pipeline.
3
+ # Run with: bundle exec ruby bench/profile_moves.rb
4
+ # Captured baseline: bench/baseline_moves.txt (via rake bench:moves)
5
+
6
+ $LOAD_PATH.unshift(File.expand_path('lib', File.join(__dir__, '..')))
7
+ require 'pgn'
8
+ require 'memory_profiler'
9
+ require 'benchmark/ips'
10
+
11
+ EXAMPLES = File.join(__dir__, '..', 'examples')
12
+ IMMORTAL = File.read(File.join(EXAMPLES, 'immortal_game.pgn'))
13
+ GAME = PGN.parse(IMMORTAL).freeze
14
+ SAN = GAME.first.moves.map(&:notation).freeze
15
+ PLY = SAN.length
16
+
17
+ puts "Workload: immortal game, #{PLY} plies"
18
+
19
+ # --- 1. Replay allocations (move application only, no parse) -----------------
20
+ replay_report = MemoryProfiler.report do
21
+ pos = GAME.first.starting_position
22
+ SAN.each { |m| pos = pos.move(m) }
23
+ end
24
+
25
+ puts "\n=== 1. Replay allocations (#{PLY} plies, no parse) ==="
26
+ puts "total_allocated objects: #{replay_report.total_allocated}"
27
+ puts "total_allocated bytes: #{replay_report.total_allocated_memsize}"
28
+
29
+ # --- 2. Board#dup share (the flat-board optimization target) ------------------
30
+ dup_report = MemoryProfiler.report { PLY.times { GAME.first.starting_position.board.dup } }
31
+
32
+ puts "\n=== 2. Board#dup x#{PLY} (target of flat-board COW) ==="
33
+ puts "total_allocated objects: #{dup_report.total_allocated}"
34
+ puts "total_allocated bytes: #{dup_report.total_allocated_memsize}"
35
+
36
+ # --- 3. Board#at(str) share (the at(str) optimization target) ---------------
37
+ start_board = GAME.first.starting_position.board
38
+ at_report = MemoryProfiler.report { 1000.times { start_board.at('e4') } }
39
+
40
+ puts "\n=== 3. Board#at(str) x1000 (target of coord-arithmetic at) ==="
41
+ puts "total_allocated objects: #{at_report.total_allocated}"
42
+ puts "total_allocated bytes: #{at_report.total_allocated_memsize}"
43
+
44
+ # --- 4. Replay throughput (fresh game each iter to defeat memoization) ------
45
+ puts "\n=== 4. Replay throughput (ips, excluding parse) ==="
46
+ Benchmark.ips do |x|
47
+ x.config(time: 5, warmup: 1)
48
+ x.report('replay immortal') do
49
+ PGN::Game.new(SAN, GAME.first.tags, GAME.first.result).positions
50
+ end
51
+ end
52
+
53
+ puts "\nDone. Compare this file against bench/baseline_moves.txt after optimizations."
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ # Measures parse and parse+replay throughput/allocations on a synthetic
3
+ # multi-game corpus. Run with: bundle exec ruby bench/profile_parse.rb
4
+ # Captured baseline: bench/baseline_parse.txt (via rake bench:parse)
5
+
6
+ $LOAD_PATH.unshift(File.expand_path('lib', File.join(__dir__, '..')))
7
+ require 'pgn'
8
+ require 'memory_profiler'
9
+ require 'benchmark/ips'
10
+
11
+ EXAMPLES = File.join(__dir__, '..', 'examples')
12
+ IMMORTAL = File.read(File.join(EXAMPLES, 'immortal_game.pgn')).strip
13
+ N = Integer(ENV.fetch('BENCH_N', '500'))
14
+ CORPUS = (IMMORTAL + "\n\n") * N
15
+
16
+ puts "Corpus: #{N} copies of the immortal game"
17
+
18
+ # --- 1. Parse-only allocations ------------------------------------------------
19
+ parse_report = MemoryProfiler.report { PGN.parse(CORPUS) }
20
+ puts "\n=== 1. Parse-only allocations (#{N} games) ==="
21
+ puts "total_allocated objects: #{parse_report.total_allocated}"
22
+ puts "total_allocated bytes: #{parse_report.total_allocated_memsize}"
23
+
24
+ # --- 2. Parse + replay allocations (real-world load) --------------------------
25
+ full_report = MemoryProfiler.report { PGN.parse(CORPUS).each(&:positions) }
26
+ puts "\n=== 2. Parse + replay allocations (#{N} games) ==="
27
+ puts "total_allocated objects: #{full_report.total_allocated}"
28
+ puts "total_allocated bytes: #{full_report.total_allocated_memsize}"
29
+
30
+ # --- 3. Parse-only throughput -------------------------------------------------
31
+ puts "\n=== 3. Parse-only throughput (ips) ==="
32
+ Benchmark.ips do |x|
33
+ x.config(time: 5, warmup: 1)
34
+ x.report("parse #{N} games") { PGN.parse(CORPUS) }
35
+ end
36
+
37
+ # --- 4. Parse + replay throughput ---------------------------------------------
38
+ puts "\n=== 4. Parse + replay throughput (ips) ==="
39
+ Benchmark.ips do |x|
40
+ x.config(time: 5, warmup: 1)
41
+ x.report("parse+replay #{N} games") { PGN.parse(CORPUS).each(&:positions) }
42
+ end
43
+
44
+ puts "\nDone. Compare this file against bench/baseline_parse.txt after optimizations."