Skip to content

v2.12.0: faster createInstanceFromMap (direct constructor invokers, opt-in resolved maps) - #57

Merged
gmpassos merged 12 commits into
masterfrom
perf/create-instance-from-map
Oct 15, 2026
Merged

gmpassos merged 12 commits into
masterfrom
perf/create-instance-from-map

Conversation

@gmpassos

@gmpassos gmpassos commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

This makes ClassReflection.createInstanceFromMap faster. bones_api calls it for every DB row, and its profile puts it at ~23% of SQLite user/list CPU.

  • Generator: a direct invoker per constructor. It calls the constructor without Function.apply:
    invoker: (a) => Foo.named(
      a[0] as String,
      x: identical(a[1], ConstructorReflection.absentArgument) ? 0 : a[1] as double,
    ),
    • Each argument is cast to its parameter type, so the generated code is clean under strict-casts.
    • An omitted optional or named parameter arrives as ConstructorReflection.absentArgument. The invoker then passes the default written in its own code, in the parameter's type context, so double x = 0 gets 0.0 and const [] becomes a List<String>.
    • A constructor whose parameter type uses a type parameter (T, List<T>) gets no invoker and keeps using Function.apply.
    • Code generated by earlier versions has no invoker and keeps using Function.apply.
  • createInstanceWithConstructor calls constructors through invokeWith(MethodInvocation). When there is no fieldValueResolver, it builds the invoker arguments straight from the map. That covers constructors without optional positional parameters, in classes without JSON name aliases.
    • The values passed are the same as before. A single "omittable parameter" rule is shared with methodInvocation.
    • Errors are reported as before, through the same retry/report logic, and the constructor isn't called again with the same arguments.
  • createInstanceFromMap(valuesResolved:, exactKeys:): options for maps whose values are already resolved (fieldValueResolver is ignored) and whose keys are the exact names (no matching on _ prefix or case).
  • Field-setting fallback (create() + setters): it no longer allocates a FieldReflection per field and row.
  • Constructor choice: 2.11.0 already caches it per map shape; unchanged here.
  • Version: bumped to 2.12.0 (v2.11.0 is tagged), and the checked-in .g.dart files are regenerated.

Review fixes (ef2fef1)

  • The first version passed the reflected ParameterReflection.defaultValue for omitted parameters. That value has the wrong type for double x = 0 (an int) and for untyped const [], so calls threw a TypeError. Defaults are now written into the generated code.
  • Invoker arguments are now cast (strict-casts).
  • A direct call that throws is no longer followed by a second call of the constructor.
  • valuesResolved is no longer passed through 4 more public signatures.

Benchmarks

benchmark/create_instance_from_map.dart (--markdown entries in benchmark/HISTORY.md). Apple M5, JIT, alternated with 2.11.0:

Case 2.11.0 2.12.0
account: constructor 3110 ns 450 ns (−85.5%)
account: constructor (valuesResolved, exactKeys) — 385 ns
account: fromJson 4454 ns 3035 ns (−31.9%)
member: create + setters 1020 ns 750 ns (−26.5%)

bones_api db_users on SQLite, ops/sec, alternated, with the reflection regenerated:

Operation bones_api unchanged with the options in instantiateFromMapImpl
user/list +8–11% +24–33%
user/byState +10.5% +22–30%
user/byId +7.7% +18–20%

Note for bones_api: instantiateFromMapImpl is also reached from JSON (e.g. storeAllFromJson), where values are not resolved (enum names, DateTime strings, lists of maps). Passing valuesResolved: true on that path breaks entities with such fields. Pass the options only where values are already resolved (DB rows).

Test plan

  • test/reflection_factory_instantiate_test.dart checks that Function.apply, the invoker via MethodInvocation, and the direct call give the same results or errors for:
    • absent and null named parameters, with and without defaults;
    • int-literal defaults of double parameters and typed const []/const {} defaults;
    • function-typed parameters;
    • required named parameters;
    • wrong types and extra keys;
    • optional positional parameters;
    • super parameters with inherited defaults;
    • JSON alias classes;
    • the create + setters fallback;
    • a reflection bound to an object;
    • exactKeys.
      It also checks that no invoker is generated for type-parameter types and that a constructor which throws is called once per attempt.
  • The build test asserts the emitted typed invoker: code.
  • dart analyze and dart format are clean, and dart test passes all 538 tests. The generated code also analyzes clean under strict-casts.
  • bones_api db_users API tests pass on 2.12.0, both unchanged and with the options.
  • A larger downstream codebase regenerated with 2.12.0 analyzes clean, and its full test suite passes with bones_api unchanged. The only failure is its ensure_build, which is expected because the reflection was regenerated.

🤖 Generated with Claude Code

gmpassos and others added 7 commits October 14, 2026 06:53
…in resolved maps (v2.12.0)

- Generator: each ConstructorReflection gets an `invoker` that calls the
  constructor directly; `invokeWith` falls back to Function.apply for code
  generated by earlier versions.
- createInstanceFromMap: `valuesResolved` and `exactKeys` options for maps
  whose keys and values are already resolved (e.g. bones_api DB rows);
  the fast path builds invoker arguments straight from the map and falls
  back to the MethodInvocation path on any error.
- Field-setting fallback: set fields through the cached no-object
  FieldReflections (setFor/setNullableFor), no per-row allocation.
- benchmark/create_instance_from_map.dart (+ README), fixture and tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benchmark/HISTORY.md, following bones_api's benchmark/db_users/HISTORY.md:
progression table, the --markdown entries of both versions (alternated
runs), and the bones_api db_users end-to-end comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lls (review of #57)

- The generated invoker writes each omitted parameter's default in its own
  code, in the parameter's type context: an omitted argument arrives as
  `ConstructorReflection.absentArgument`. Passing the reflected
  `ParameterReflection.defaultValue` broke `double x = 0` (an `int`) and
  `List<String> l = const []` (a `List<dynamic>`) with a TypeError.
- Invoker arguments are cast to their parameter types (`strict-casts`
  clean); constructors with parameters typed by a type parameter get no
  invoker (Function.apply).
- A direct call that throws is reported through the MethodInvocation path's
  retry/report logic without calling the constructor again.
- The direct call applies whenever there is no `fieldValueResolver`, so
  `valuesResolved` is only a `createInstanceFromMap` option (no longer
  threaded through 4 more signatures).
- One "omittable parameter" rule shared by `methodInvocation` and the
  direct call.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ugs they found

Tests:
- Defaults whose reflected value loses its type (Set, Map<String, List<int>>,
  Duration, enum, a static const by its bare name, a nullable parameter with
  a non-null default); fromJson with typed defaults; parameters typed by a
  type parameter (no invoker); the `valuesResolved` contract (no
  conversion); a throwing / case-insensitive fieldNameResolver on the
  direct path; MethodInvocations that don't fit the constructor.
- generated_code_test: a package analyzed with strict-casts and
  avoid_dynamic_calls (--fatal-infos), with defaults from a prefixed import,
  a static const and an inherited super-parameter, and a program that runs
  the invokers (direct and JSON paths) and checks the runtime types.

Generator fixes (generated code that didn't compile):
- A default value referencing a static member by its bare name is
  qualified by its class.
- A function-typed field using a class type parameter is reflected as
  `dynamic`.
- supperTypes from a prefixed import keep the prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter compiles

Reflected as `dynamic` (not `Function`): a `Function?` setter value can't be
assigned to a `T Function()?` field (no implicit downcast).

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

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.31313% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.53%. Comparing base (77164c6) to head (5411694).

Files with missing lines Patch % Lines
lib/src/reflection_factory_builder.dart 69.67% 37 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #57      +/-   ##
==========================================
+ Coverage   92.56%   93.53%   +0.97%     
==========================================
  Files          14       14              
  Lines        6428     6532     +104     
==========================================
+ Hits         5950     6110     +160     
+ Misses        478      422      -56     
Flag Coverage Δ
unittests 93.53% <81.31%> (+0.97%) ⬆️

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 4 commits October 14, 2026 08:22
…h the raw class

`T Function()? make` was reflected as `dynamic`; it's now the type the
generated code reads it as, through the raw class (type parameters as their
bounds): `dynamic Function()?`, or `Base Function(int, {String? label})?` for
`T extends Base`. Built from its parts (qualified types, nullability of the
function type, its parameters and return type kept); generic function types
stay `dynamic`. Its generated setter compiles under strict-casts.

e2e: settable and final function fields, a prefixed bound, positional /
optional / named / required parameters, and every nullability shape, checked
in the generated code and at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nherited function-typed fields

- Dartdocs with examples for _Field.typeNameAsNullableCode /
  _typeThroughRawReceiver and the DartType helpers typeNameAsCode,
  typeNameAsNullableCode, functionTypeAsCode, hasTypeParameter,
  isResolvableType, isParameterType, isNullable.
- A subclass with the type argument (`extends Box<int>`): its invoker, and
  its inherited `T Function()?` field reflected as `int Function()?`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `V` of `FieldReflection<O, V>` for any field whose type uses a class type
parameter (`T`, `List<T>`, `Map<String, T>`, function types) is the type the
generated code reads it as through the raw class: type parameters as their
bounds, or as the type arguments of a subclass or mixin application
(`IntBox extends Box<int>`: `int?`, `List<int>`). Their setters receive the
type they assign and compile under strict-casts (they didn't for a bounded
`T`). `TypeReflection`s (JSON decoding) are unchanged.

- `qualifiedTypeCode`: types written with qualified type arguments and
  function types, keeping non-generic typedef names.
- Generated: TestOpB.value `dynamic` -> `double?`, TestIntBox.items
  `List<dynamic>` -> `List<int>`.
- e2e (strict-casts): settable bounded/unbounded `T?`, `List<T>`,
  `Map<String, T Function()?>`, a typedef parameter, and an `IntBox`
  subclass, checked in code and at runtime; unit tests for the inherited
  and mixin field types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t independent of checkout file times

- test_chrome: an `int` for a `double` fits on the web (both are JS
  numbers), so the `valuesResolved` contract test now uses values that
  don't fit on any platform (a `String` for a `double`, a `List<dynamic>`
  for a `List<String>`), each checked on its own.
- test_vm (flaky): the inspector test compared the checked-in sources' and
  generated files' times, which in a fresh checkout only reflect the order
  `git` wrote them in. It now gives the generated files the current time
  (checking them by version), and a new test covers expiry by time and by
  version with files whose times are set.

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

- Many parameters of the same type (named, not in alphabetical order):
  each value in its position on every path.
- Generic classes with type arguments (as entity references), lists of
  entities, maps, a typedef, nullable elements, enums, `Object?`, `dynamic`
  and records; type arguments that don't fit fail as with `Function.apply`.
- Defaults: static method and top-level tear-offs, negatives, escapes,
  const collections and instances.
- `const`, `factory`, redirecting and redirecting `factory` constructors.
- Constructor choice: the first constructor that fits a value (with usage
  sorting), a non-mandatory `@JsonConstructor`, JSON name aliases.
- `create()` + setters: final fields, nullables set to `null`, lists.
- `valuesResolved` with a `fieldNameResolver` (snake case), alternating key
  shapes and modes, the `ReflectionFactory` registry, and parameter types
  vs field types.
- e2e (strict-casts): a generic class from a prefixed import, records, and
  function defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gmpassos
gmpassos merged commit 242e245 into master Oct 15, 2026
4 of 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