Skip to content

perf: reuse the encoding the after image already built - #59

Merged
cardmagic merged 1 commit into
mainfrom
perf/reuse-the-committed-encoding
Aug 30, 2026
Merged

perf: reuse the encoding the after image already built#59
cardmagic merged 1 commit into
mainfrom
perf/reuse-the-committed-encoding

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

Follow-up to #58, which cut the per-turn state copies from three to two and
left one redundant traversal behind. This removes it.

Version 0.14.4.

What changed

State#to_h copies the state by encoding it and parsing the result, then
discarded the encoded string. complete then normalized and encoded the same
hash a second time, only to learn its byte size for max_state_bytes and for
solid_objects.state.large.

to_h_with_byte_size returns the copy beside the size of the encoding that
produced it. A committed turn now traverses the state twice and encodes it
twice, against three of each before.

  • Serialization.deep_copy_with_byte_size is new; deep_copy calls it, so
    both encode once.
  • max_state_bytes is enforced against the reported size rather than by
    encoding the state again. The turn still fails with PayloadTooLarge before
    it opens its commit transaction.

Measured

Apple M5, Ruby 4.0.5, Rails 8.1.3.1, SQLite 3.53.2. One hot actor, 300 messages
per size, median of five runs, the two trees run one after the other in each
round, against the released 0.14.3.

Committed state 0.14.3 0.14.4 Change
23 bytes 1,251.2 messages/s 1,242.8 messages/s -0.7%
13,662 bytes 612.0 messages/s 644.7 messages/s +5.3%
118,786 bytes 171.9 messages/s 199.6 messages/s +16.1%
1,026,356 bytes 23.4 messages/s 26.2 messages/s +12.0%

The empty-state row sits inside run-to-run variance.

Effects to review

  • API: Serialization.deep_copy_with_byte_size and
    State#to_h_with_byte_size are new. deep_copy and to_h keep their
    signatures and return values.
  • Correctness: no behavior change intended. The byte size measured is the
    same JSON encoding of the same normalized state that dump_with_byte_size
    produced.
  • Migration: none.

Tests

Written first, each watched fail:

SerializationTest#test_reports_the_encoded_byte_size_beside_a_deep_copy
NoMethodError: undefined method 'deep_copy_with_byte_size'

StateCommitTest#test_a_committed_message_does_not_encode_the_state_a_second_time
Expected: 1
  Actual: 2

test_a_state_above_the_hard_limit_fails_its_message_and_commits_nothing is
new and passed against main before the change, deliberately: it pins the
max_state_bytes behavior this diff moves, which nothing covered before.

The two copy-count tests now count to_h_with_byte_size as well as to_h,
because the after image comes from the new method; the property they assert,
one full copy after the handler, is unchanged.

Validation

bundle exec rake   # 607 runs, 1960 assertions, 0 failures, 0 errors, 15 skips
                   # standard, rubocop, rbs validate, steep, brakeman clean
COUNT=300 bundle exec ruby -Ilib benchmark/state_size.rb

Parity

solid-objects-js already measures the string it commits, so no port is
needed; this brings the gem to the same shape.

Copying the state encodes it and parses the result, then threw the
encoded string away. complete then normalized and encoded the same hash
a second time, only to learn its size. A committed turn therefore
traversed the state three times and encoded it three times, where two of
each are necessary.

to_h_with_byte_size returns the copy beside the size of the encoding
that produced it, and the commit enforces max_state_bytes against that
size rather than by encoding again. The turn still fails with
PayloadTooLarge before it opens its transaction, which a test now covers
end to end, because nothing covered the hard limit before.

Measured on SQLite against the released 0.14.3 tree, medians of five
interleaved runs: 5.3% more committed messages per second at 13 KB of
state, 16.1% at 116 KB, and 12.0% at 1 MB.

solid-objects-js already measures the string it commits. This brings the
gem to the same shape.
@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown

Greptile Summary

The PR removes a redundant state serialization by carrying the encoded byte size alongside the deep-copied after-image.

  • Adds Serialization.deep_copy_with_byte_size and State#to_h_with_byte_size.
  • Reuses the captured after-image for persistence, state-size enforcement, and large-state instrumentation.
  • Updates generated RBS declarations, tests, benchmarks, changelog, lockfile, and gem version.

Confidence Score: 5/5

The PR appears safe to merge with no actionable correctness, security, or repository-rule issues identified.

The reused value and byte size come from the same normalized JSON encoding, preserve the previous strict-greater-than size boundary, and are checked before the commit transaction.

Important Files Changed

Filename Overview
lib/solid_objects/serialization.rb Adds a shared deep-copy operation that returns the parsed copy and the byte size of the encoding used to create it.
lib/solid_objects/state.rb Exposes the new deep-copy result through to_h_with_byte_size while preserving to_h.
lib/solid_objects/executor.rb Reuses the captured after-image and byte size for mutation checks, limit enforcement, persistence, and instrumentation without changing established boundary behavior.
test/integration/state_commit_test.rb Covers hard-limit rollback behavior and verifies that committed state is not redundantly encoded.
test/unit/serialization_test.rb Covers the returned byte size, copy independence, and single-encoding behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Actor state after handler] --> B[Normalize and encode once]
  B --> C[Parse independent after-image]
  B --> D[Capture encoded byte size]
  D --> E{Within max_state_bytes?}
  E -- No --> F[Raise PayloadTooLarge]
  E -- Yes --> G[Commit parsed after-image]
  D --> H[Report large-state metric]
Loading

Reviews (1): Last reviewed commit: "perf: reuse the encoding the after image..." | Re-trigger Greptile

@cardmagic
cardmagic merged commit dd72e6e into main Aug 30, 2026
41 checks passed
@cardmagic
cardmagic deleted the perf/reuse-the-committed-encoding branch August 30, 2026 03:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant