[BAC-1484] Speed up Entity.represent for large nested payloads (~3-4x) - #26
Open
taleh007 wants to merge 2 commits into
Open
[BAC-1484] Speed up Entity.represent for large nested payloads (~3-4x)#26taleh007 wants to merge 2 commits into
taleh007 wants to merge 2 commits into
Conversation
Precompute [json_name, name, type, custom_serializer] tuples once per class instead of camelizing names and looking up serializers on every represent call. Read hash fields via string-then-symbol lookup instead of copying each hash with_indifferent_access (kept for classes with custom serializers, whose blocks receive the hash), and skip the options merge when no options are passed.
taleh007
marked this pull request as ready for review
August 7, 2026 11:30
HolyWalley
approved these changes
Aug 7, 2026
| config.example_status_persistence_file_path = ".rspec_status" | ||
|
|
||
| # Performance specs are opt-in: PERFORMANCE=1 bundle exec rspec spec/performance | ||
| config.filter_run_excluding :performance unless ENV["PERFORMANCE"] |
Contributor
There was a problem hiding this comment.
is it take much to run? maybe make them on by default?
yard
reviewed
Aug 7, 2026
yard
left a comment
Contributor
There was a problem hiding this comment.
Does the dynamically emitted serialization/deserialization code still apply? Any optimizations to be made there?
| if object_or_hash.is_a?(Hash) | ||
| object_or_hash[name] | ||
| value = object_or_hash[name] | ||
| value.nil? ? object_or_hash[name.to_sym] : value |
Contributor
There was a problem hiding this comment.
Since we are talking optimizations here, shall this “try this, then try that” be gone too?
yard
reviewed
Aug 7, 2026
yard
left a comment
Contributor
There was a problem hiding this comment.
Also pt. 2 (killing with_indifferent_access everywhere but for custom serilizer block) does look a tad weird, we probably don’t really gain much from it but now implement it ourselves
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Entity.representwas slow on large payloads. This PR makes it ~4x faster for hash sources and ~3x faster for entity object graphs, and adds an opt-in performance spec suite that documents the baseline.Benchmark — one catalog entity holding 5000 products, each with regular fields plus a small nested entity (min / avg over 5 runs, same machine):
Why it works (in simple terms)
Representing a list of 5000 products means calling
represent10,001 times (the root + each product + each product's nested entity). The old code redid the same preparation inside every one of those calls — preparation whose result never changes:It kept re-translating the same column names. Every product has the same attributes, and
created_atcamelizes tocreatedAtevery single time — yet the inflector ran again for every attribute of every object (~50,000 times per call). Now each class computes its "represent plan" once — the output key, the type and the custom serializer for each attribute — and every later call just walks that precomputed list. Like a spreadsheet: you translate the header row once, not once per row. (Same trick as the compiled JSON assigner from BAC-1267 — and like it, the plan is built per exact class and invalidated ifattribute/serializesis called later.)It kept photocopying hashes just to read them. Every nested hash was wrapped in
with_indifferent_access— a full copy — only so fields could be found whether the keys are strings or symbols. Now we simply try the string key first and the symbol key second; no copy at all. The one place that genuinely relies on the copy —serializesblocks, which receive the source hash and may look fields up either way — still gets it, so their behavior is unchanged.It allocated option hashes nobody asked for.
default_represent_options.merge(options)ran for every nested entity even whenoptionswas empty. The merge is now skipped when there is nothing to merge.None of this changes what
representreturns — it just stops paying per object for work that is per class. One observable nuance: untyped hash attributes now pass the original hash through instead of an indifferent-access copy (identical once rendered to JSON).Performance specs
New opt-in suite, excluded from the default run (CI is unaffected):
It represents the 5000-product catalog from both source shapes, asserts the output is correct, prints the timings, and fails only if a run exceeds a deliberately generous 2s budget — a catastrophic-regression guard (e.g. accidentally quadratic serialization), not a microbenchmark.
Jira
BAC-1484
🤖 Generated with Claude Code