pgn2 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8725cb68fa694a068cba0ee2226fa90b7b9018d18dfd8cbdc61f6f560c3e12e3
4
- data.tar.gz: f551b1645e9559df049e2d87da4a99da0ab64ae05d031446d111876be5d1b7b9
3
+ metadata.gz: b17339791fa63da88b55a180d613c124509f7c286552fd9bb62dcea162eb3325
4
+ data.tar.gz: 2283fa7d6fbf06d54bebbdb9675427c2c51743627423db6bdd91df71a8e2926f
5
5
  SHA512:
6
- metadata.gz: 279e222b2cd8a736eb82979ab195b9983d79d3d61f8e1239f13de286efd5f42d6e5ef06b5d59fe0c8e36e323b12bb5567cf3995594ed2ed4eee60bcb70dcc521
7
- data.tar.gz: 1f49fb6a4b4d18d161436cb0c2c91df7aab9e1203d8896095e912d12e0623cb7adae73ded9a147dde051ccc3f8af6a2a3ef804dd4f34f96d05c8ca94da954048
6
+ metadata.gz: 311d858dca8016414e862b016fe694dff758ea575194d1a4256ebfa02e4427bc389ca291db5eb4a4cf547839e74e717aaeaed68fece0b43a1faf840148b87235
7
+ data.tar.gz: a0ad909b77394c5d7982db61ff37eb4ff02b2e1241b942e48b79bf290c70f5e6e756432fed01a5041179f27546faa2edae2a3b0cd54ff9a7f13a6bb3ac95c006
@@ -0,0 +1,103 @@
1
+ name: Release
2
+
3
+ # Auto-releases on any push to `main` whose version (lib/pgn/version.rb) does
4
+ # not yet have a matching `v<x.y.z>` tag. It gates on the RSpec matrix, then
5
+ # creates the tag and pushes the gem to rubygems.org in this same workflow
6
+ # (a tag pushed by the GITHUB_TOKEN does NOT trigger other workflows, so the
7
+ # publish happens here directly to avoid that limitation).
8
+ #
9
+ # Manual releases still work: `git tag vX.Y.Z && git push origin vX.Y.Z` fires
10
+ # publish.yml (human-pushed tags do trigger workflows).
11
+ #
12
+ # Requires the RUBYGEMS_API_KEY repository secret (already set).
13
+
14
+ on:
15
+ push:
16
+ branches: [main]
17
+
18
+ jobs:
19
+ check:
20
+ name: Detect version bump
21
+ runs-on: ubuntu-latest
22
+ outputs:
23
+ version: ${{ steps.ver.outputs.version }}
24
+ should_release: ${{ steps.ver.outputs.should_release }}
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - id: ver
28
+ run: |
29
+ version="$(ruby -Ilib -rpgn/version -e 'print PGN::VERSION')"
30
+ echo "version=$version" >> "$GITHUB_OUTPUT"
31
+ if git ls-remote --tags origin "refs/tags/v$version" | grep -q .; then
32
+ echo "should_release=false" >> "$GITHUB_OUTPUT"
33
+ echo "v$version already tagged; skipping release"
34
+ else
35
+ echo "should_release=true" >> "$GITHUB_OUTPUT"
36
+ echo "v$version is new; will release"
37
+ fi
38
+
39
+ test:
40
+ name: RSpec (ruby ${{ matrix.ruby }})
41
+ needs: check
42
+ if: needs.check.outputs.should_release == 'true'
43
+ runs-on: ubuntu-latest
44
+ strategy:
45
+ fail-fast: true
46
+ matrix:
47
+ ruby: ['3.1', '3.2', '3.3', '3.4']
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ - uses: ruby/setup-ruby@v1
51
+ with:
52
+ ruby-version: ${{ matrix.ruby }}
53
+ bundler-cache: true
54
+ - run: bundle exec rspec
55
+
56
+ release:
57
+ name: Tag & publish
58
+ needs: [check, test]
59
+ if: needs.check.outputs.should_release == 'true'
60
+ runs-on: ubuntu-latest
61
+ environment: release
62
+ permissions:
63
+ contents: write
64
+ steps:
65
+ - uses: actions/checkout@v4
66
+ - uses: ruby/setup-ruby@v1
67
+ with:
68
+ ruby-version: '3.3'
69
+ bundler-cache: true
70
+
71
+ - name: Create and push tag v${{ needs.check.outputs.version }}
72
+ run: |
73
+ git config user.name "github-actions[bot]"
74
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
75
+ git tag "v${{ needs.check.outputs.version }}" -m "v${{ needs.check.outputs.version }}"
76
+ git push origin "v${{ needs.check.outputs.version }}"
77
+
78
+ - name: Build gem
79
+ run: bundle exec rake build
80
+
81
+ - name: Configure RubyGems credentials
82
+ run: |
83
+ mkdir -p "$HOME/.gem"
84
+ printf -- ":rubygems_api_key: %s\n" "${RUBYGEMS_API_KEY}" > "$HOME/.gem/credentials"
85
+ chmod 0600 "$HOME/.gem/credentials"
86
+ env:
87
+ RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
88
+
89
+ - name: Push gem
90
+ run: gem push pkg/*.gem
91
+
92
+ - name: Verify publication
93
+ continue-on-error: true
94
+ run: |
95
+ ver="${{ needs.check.outputs.version }}"
96
+ for i in 1 2 3 4 5 6; do
97
+ if gem fetch pgn2 --version "$ver" --platform gem >/dev/null 2>&1; then
98
+ echo "pgn2 $ver published to rubygems.org"
99
+ exit 0
100
+ fi
101
+ sleep 5
102
+ done
103
+ echo "gem push succeeded but fetch verify timed out; check https://rubygems.org/gems/pgn2"
data/.gitignore CHANGED
@@ -17,4 +17,5 @@ test/version_tmp
17
17
  tmp
18
18
 
19
19
 
20
- tags
20
+ tags
21
+ *.output
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 (2026-08-13)
4
+
5
+ ### Summary
6
+
7
+ Performance quick wins ("Approach A"): safe, behavior-compatible
8
+ micro-optimizations on top of the 1.0 parser. No public API changes; serialized
9
+ PGN/FEN output stays byte-identical. See
10
+ `docs/superpowers/specs/2026-08-13-pgn-performance-quick-wins-design.md` and
11
+ `bench/IMPROVEMENTS.md` for the design and per-step deltas.
12
+
13
+ ### Changed
14
+ - **Lexer hot path**: `PGN::Lexer#next_token_pair` returns `[type, value]`
15
+ without allocating a `Token` Struct (or its `keyword_init` Hash), via a shared
16
+ scanning routine that preserves `game_starts` for verbatim `pgn` slicing.
17
+ `PgnParser#next_token` uses it.
18
+ - **`PGN::Game#moves=`**: reuses an existing `MoveText` when its comment is
19
+ already clean (halving `MoveText` allocations on the parse path); still
20
+ re-wraps to preserve the legacy double-`clean_text` for multi-line/nested
21
+ comments.
22
+ - **`PGN::Move#piece=`**: non-allocating castling guard (`start_with?('O')`
23
+ instead of `match('O-O')`), removing a `MatchData` from every `Move.new`.
24
+ - **`PGN::Position`**: `next_player` uses a ternary instead of
25
+ `(PLAYERS - [player])`; `Position#move` skips `castling - restrictions` when
26
+ there are no restrictions.
27
+ - **`PGN::MoveCalculator`**: memoized `destination_coords`, frozen
28
+ `ROOK_RESTRICTIONS` constant, empty short-circuit in `castling_restrictions`.
29
+ - **`PGN::Move#pawn?`**: non-allocating (`==` instead of `%w[P p].include?`).
30
+ - Performance vs 1.0: replay allocations −21% objects / −27% bytes; parse-only
31
+ allocations −3.6% objects / −28% bytes; parse + replay −15% objects / −28%
32
+ bytes. The full hand-rolled SAN parser was deferred (marginal payoff, high
33
+ risk) per the design's "measure first" guidance.
34
+
3
35
  ## 1.0.0 (2026-08-13)
4
36
 
5
37
  ### Summary
data/README.md CHANGED
@@ -156,32 +156,37 @@ snapshots captured before any hot-path work — at that point pgn2's parser
156
156
  were byte-for-byte the original gem's code. The "pgn2" figures are the
157
157
  current committed baselines (stdlib `Racc` + `StringScanner` parser,
158
158
  column-level copy-on-write `Board`, getbyte-arithmetic `at`). All numbers
159
- are from the same machine (Ruby 4.0.5, x86_64-linux); allocation counts are
160
- deterministic, throughput is over a 5 s window so treat ms/i as the stable
161
- signal (ips is noisy).
159
+ are from the same machine (Ruby 4.0.5, x86_64-linux). **Allocation counts
160
+ are deterministic** and the headline signal. **Throughput is reported as the
161
+ median of N back-to-back wall-clock runs** (whittle-era commit `1360bfc` vs
162
+ current `main`): the `benchmark-ips` harness has ±30–60% per-run variance, so
163
+ its single-run `ms/i` is not reliable across versions — e.g. the Racc baseline
164
+ once recorded 371 ms/i (±37%) for parse+replay but the same commit measures
165
+ ~1017 ms reproduced today. `bench/baseline_*.txt` remains the harness of
166
+ record for allocations.
162
167
 
163
168
  Move pipeline — immortal game, 45 plies (`bench/profile_moves.rb`):
164
169
 
165
170
  | Metric | original `pgn` | pgn2 | Δ |
166
171
  |---|---:|---:|---:|
167
- | Replay allocations (objects) | 5124 | 2565 | -2559 (-50.0%) |
168
- | Replay allocations (bytes) | 262608 | 155296 | -107312 (-40.9%) |
169
- | `Board#dup` x45 (objects) | 451 | 91 | -360 (-79.8%) |
170
- | `Board#dup` x45 (bytes) | 43096 | 6736 | -36360 (-84.4%) |
172
+ | Replay allocations (objects) | 5124 | 1710 | -3414 (-66.7%) |
173
+ | Replay allocations (bytes) | 262608 | 103760 | -158848 (-60.5%) |
174
+ | `Board#dup` x45 (objects) | 451 | 136 | -315 (-69.8%) |
175
+ | `Board#dup` x45 (bytes) | 43096 | 10336 | -32760 (-76.0%) |
171
176
  | `Board#at(str)` x1000 (objects) | 6000 | 0 | -6000 (-100%) |
172
177
  | `Board#at(str)` x1000 (bytes) | 240000 | 0 | -240000 (-100%) |
173
- | Replay throughput | 1.132k ips (884 µs/i) | 1.669k ips (599 µs/i) | ~1.5x faster |
178
+ | Replay throughput | 841 µs/i | 766 µs/i | ~1.10x faster |
174
179
 
175
180
  Parser — 500 immortal games (`bench/profile_parse.rb`):
176
181
 
177
182
  | Metric | original `pgn` | pgn2 | Δ |
178
183
  |---|---:|---:|---:|
179
- | Parse-only allocations (objects) | 1248065 | 557035 | -691030 (-55.4%) |
180
- | Parse-only allocations (bytes) | 120370470 | 36636902 | -83733568 (-69.6%) |
181
- | Parse + replay allocations (objects) | 3778073 | 1614087 | -2163986 (-57.3%) |
182
- | Parse + replay allocations (bytes) | 249570048 | 105404152 | -144165896 (-57.7%) |
183
- | Parse-only throughput | 1.513 ips (661 ms/i) | 4.711 ips (212 ms/i) | ~3.1x faster |
184
- | Parse + replay throughput | 0.933 ips (1070 ms/i) | 2.698 ips (371 ms/i) | ~2.9x faster |
184
+ | Parse-only allocations (objects) | 1248065 | 603537 | -644528 (-51.6%) |
185
+ | Parse-only allocations (bytes) | 120370470 | 28257414 | -92113056 (-76.5%) |
186
+ | Parse + replay allocations (objects) | 3778073 | 1427586 | -2350487 (-62.2%) |
187
+ | Parse + replay allocations (bytes) | 249570048 | 78104136 | -171465912 (-68.7%) |
188
+ | Parse-only throughput | 1461 ms/i | 327 ms/i | ~4.5x faster |
189
+ | Parse + replay throughput | 1938 ms/i | 826 ms/i | ~2.35x faster |
185
190
 
186
191
  What changed to get there:
187
192
 
@@ -193,9 +198,22 @@ What changed to get there:
193
198
  6. Parser — `whittle` (≈80% of parse allocations) replaced by stdlib `Racc` +
194
199
  `StringScanner`; `PGN::Game#pgn` sliced from per-game byte offsets (no O(n²)
195
200
  `@@pgn +=` accumulation).
201
+ 7. `PGN::Lexer#next_token_pair` — parser hot path returns `[type, value]`
202
+ without allocating a `Token` Struct (or its `keyword_init` Hash), via a
203
+ shared scanning routine that preserves `game_starts` for verbatim `pgn` slicing.
204
+ 8. `PGN::Game#moves=` — reuse existing `MoveText` when its comment is already
205
+ clean; halve `MoveText` allocations on the parse path (still re-wraps to
206
+ preserve the legacy double-`clean_text` for multi-line/nested comments).
207
+ 9. `PGN::Move#piece=` — non-allocating castling guard (`start_with?('O')`
208
+ instead of `match('O-O')`), removing a `MatchData` from every `Move.new`.
209
+ 10. `PGN::Position#next_player` — ternary instead of `(PLAYERS - [player])`;
210
+ `Position#move` skips `castling - restrictions` when there are none.
211
+ 11. `PGN::MoveCalculator` — memoized `destination_coords`, frozen
212
+ `ROOK_RESTRICTIONS`, empty short-circuit in `castling_restrictions`;
213
+ `Move#pawn?` non-allocating.
196
214
 
197
215
  Public output (FEN, PGN) is byte-identical to the original gem; the full
198
- suite (187 examples) stays green. See `bench/IMPROVEMENTS.md` for the per-step
216
+ suite (182 examples) stays green. See `bench/IMPROVEMENTS.md` for the per-step
199
217
  before/after deltas that produced these tables.
200
218
 
201
219
  ## Installation
data/TODO.md CHANGED
@@ -10,3 +10,12 @@
10
10
 
11
11
  - Support converting a game to pgn format
12
12
  - Speed up parsing
13
+ - Collapse PGN::Lexer's per-token allocation chain (Array in `scan_one` ->
14
+ `Token` struct -> Array in racc's `next_token`) to fewer allocations on
15
+ the hot path; keep the full `Token` (with offset/line) only for the
16
+ `#tokens` spec helper, which is its only consumer.
17
+ - Replace the right-recursive `tag_section`/`variation_list` rules in
18
+ `pgn_parser.y` with ordinary left-recursion plus one explicit `.reverse`
19
+ at the point each list is consumed, so the legacy whittle-order
20
+ compatibility quirk is a single greppable line instead of implicit in
21
+ recursion direction.
@@ -73,3 +73,71 @@ inputs during the migration; 32 explicit parser specs now pin the behavior
73
73
  permanently (`spec/parser_explicit_spec.rb`).
74
74
 
75
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,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.
@@ -1,12 +1,12 @@
1
1
  Workload: immortal game, 45 plies
2
2
 
3
3
  === 1. Replay allocations (45 plies, no parse) ===
4
- total_allocated objects: 2565
5
- total_allocated bytes: 155296
4
+ total_allocated objects: 1710
5
+ total_allocated bytes: 103760
6
6
 
7
7
  === 2. Board#dup x45 (target of flat-board COW) ===
8
- total_allocated objects: 91
9
- total_allocated bytes: 6736
8
+ total_allocated objects: 136
9
+ total_allocated bytes: 10336
10
10
 
11
11
  === 3. Board#at(str) x1000 (target of coord-arithmetic at) ===
12
12
  total_allocated objects: 0
@@ -15,8 +15,8 @@ total_allocated bytes: 0
15
15
  === 4. Replay throughput (ips, excluding parse) ===
16
16
  ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
17
17
  Warming up --------------------------------------
18
- replay immortal 96.000 i/100ms
18
+ replay immortal 112.000 i/100ms
19
19
  Calculating -------------------------------------
20
- replay immortal 1.669k46.5%) i/s (599.18 μs/i) - 8.352k in 5.004383s
20
+ replay immortal 1.178k 4.6%) i/s (848.64 μs/i) - 5.936k in 5.037516s
21
21
 
22
22
  Done. Compare this file against bench/baseline_moves.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.
@@ -1,25 +1,25 @@
1
1
  Corpus: 500 copies of the immortal game
2
2
 
3
3
  === 1. Parse-only allocations (500 games) ===
4
- total_allocated objects: 1248065
5
- total_allocated bytes: 120370470
4
+ total_allocated objects: 603537
5
+ total_allocated bytes: 28257414
6
6
 
7
7
  === 2. Parse + replay allocations (500 games) ===
8
- total_allocated objects: 2498573
9
- total_allocated bytes: 195914048
8
+ total_allocated objects: 1427586
9
+ total_allocated bytes: 78104136
10
10
 
11
11
  === 3. Parse-only throughput (ips) ===
12
12
  ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
13
13
  Warming up --------------------------------------
14
14
  parse 500 games 1.000 i/100ms
15
15
  Calculating -------------------------------------
16
- parse 500 games 1.349 0.0%) i/s (741.33 ms/i) - 7.000 in 5.189280s
16
+ parse 500 games 3.64927.4%) i/s (274.08 ms/i) - 19.000 in 5.207546s
17
17
 
18
18
  === 4. Parse + replay throughput (ips) ===
19
19
  ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
20
20
  Warming up --------------------------------------
21
21
  parse+replay 500 games 1.000 i/100ms
22
22
  Calculating -------------------------------------
23
- parse+replay 500 games 0.897 (± 0.0%) i/s (1.11 s/i) - 5.000 in 5.572640s
23
+ parse+replay 500 games 1.399 (± 0.0%) i/s (715.05 ms/i) - 7.000 in 5.005330s
24
24
 
25
25
  Done. Compare this file against bench/baseline_parse.txt after optimizations.