Skip to content

v2.13.0: faster instantiation from a Map (JSON entity decoding) - #61

Merged
gmpassos merged 3 commits into
masterfrom
perf/json-decode-revived-args
Oct 15, 2026
Merged

gmpassos merged 3 commits into
masterfrom
perf/json-decode-revived-args

Conversation

@gmpassos

Copy link
Copy Markdown
Owner

Faster createInstanceFromMap / JsonDecoder entity decoding, mostly for code compiled to JavaScript (the browser), and also on Wasm and the VM. The created instances are the same.

Changes

  • Direct calls with a fieldValueResolver. A constructor that can be called directly (canInvokeFromResolvedMap) is now also called with the values revived by a fieldValueResolver, once per parameter, instead of through MethodInvocation (_revivedMapArguments). Unresolved values (a non-null value revived to null, or an entity list with a null) and resolvers that throw still take the MethodInvocation path, which resolves them again and reports them as before.
  • Maps of the same shape. The fields-to-keys resolution, best constructors and fields to set are kept for the last map shape of each class (the same keys, in the same order, and the same FieldNameResolver), and reused for the next maps of that shape. FieldNameResolver now documents that its result must depend only on the field name and the map keys.
  • createInstanceFromMap direct path: for a map of the last shape with a single constructor, the constructor is called without the intermediate calls. A constructor error is reported as before, without a second call. The helpers are static, because dart2js (without --omit-implicit-checks) checks the parameter types of instance methods on every call, and of static functions it doesn't.
  • JsonDecoder: a decoded JSON object (Map<String, dynamic>) is no longer copied before decoding (is Map<String, Object> was false for it).
  • JsonEntityCacheSimple.getEntityID: the ID field is found once per type.
  • ClassReflection.getField(name, obj): no FieldReflection is allocated per call.

Benchmark

bones_api's benchmark/db_users/bin/json_decode_benchmark.dart decodes a JSON list of 1000 users (each with an address and 2 roles) as a module proxy does. µs per user:

Target 2.12.2 2.13.0
JavaScript (dart2js -O1 --minify, the webdev build --release default) 27.4 11.8 (−57%)
Wasm (dart2wasm -O2 --minify) 6.5 3.7 (−42%)
VM 7.5 3.9 (−48%)

These include the bones_api changes in its companion PR. The reflection_factory part is most of it, about −45% in JS.

Tests

581 pass (dart test). The instantiation and JSON tests also pass compiled to JS (-p node). New tests cover:

  • revived values: called once per parameter, unresolved values, resolvers that throw, constructor errors
  • maps of the same shape in sequence, compared with each map decoded alone, for both constructors and setters, with and without resolvers
  • a resolver of another identity on the same shape
  • getField of another object
  • getEntityID

The existing equivalence helpers now also compare against the legacy MethodInvocation path with a reviver.

v2.13.0: version bumped, generated code regenerated (version lines only), CHANGELOG added.

🤖 Generated with Claude Code

- createInstanceWithConstructor: a constructor that can be called directly
  is also called with values revived by a fieldValueResolver (once per
  parameter), instead of through MethodInvocation. Unresolved values and
  resolvers that throw still take the MethodInvocation path.
- The fields-to-keys resolution, best constructors and fields to set are
  kept for the last map shape (same keys, order and FieldNameResolver) of
  each class. FieldNameResolver: documented that it must depend only on
  the field name and the map keys.
- createInstanceFromMap: a static direct path for a map of the last shape
  with a single constructor (static helpers: no implicit parameter checks
  in dart2js). A constructor error is reported as before, without a
  second call.
- JsonDecoder: a decoded Map<String, dynamic> is no longer copied.
- JsonEntityCacheSimple.getEntityID: the ID field is found once per type.
- ClassReflection.getField(name, obj): no FieldReflection per call.

Decoding 1000 users (bones_api benchmark), us per user: JS (dart2js -O1
--minify) 27.4 -> 11.8, Wasm 6.5 -> 3.7, VM 7.5 -> 3.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.65%. Comparing base (eb94ee8) to head (d6d0d19).

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #61      +/-   ##
==========================================
+ Coverage   93.55%   93.65%   +0.10%     
==========================================
  Files          14       14              
  Lines        6544     6652     +108     
==========================================
+ Hits         6122     6230     +108     
  Misses        422      422              
Flag Coverage Δ
unittests 93.65% <100.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

gmpassos and others added 2 commits October 15, 2026 07:13
When the fieldValueResolver left a value unresolved on the
createInstanceFromMap direct path, it fell back to
createInstanceWithBestConstructor, which tried the revived direct call
again before the MethodInvocation path: the resolver ran over every
parameter one more time (decoding nested entities again). It now goes
straight to the MethodInvocation path (directUnresolved).

Tests (edge cases): reviver call counts on the unresolved path (8 -> 6),
a revived value for an absent parameter, an entity list with a null,
the same class created inside a resolver (cached shape replaced
mid-call), disposeCache between maps, getBestConstructorsForMap with
other options in between, JsonDecoder with Map<String, dynamic> (not
modified) and other key types, lists with nulls and mixed shapes, and
getEntityID by the first int field or without an ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gmpassos
gmpassos merged commit f9af318 into master Oct 15, 2026
5 checks passed
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