Skip to content

perf: cache source member attribute metadata during compilationPerf/compilation scoped attribute cache - #1022

Open
vb-kalei wants to merge 1 commit into
MapsterMapper:developmentfrom
vb-kalei:perf/compilation-scoped-attribute-cache
Open

vb-kalei wants to merge 1 commit into
MapsterMapper:developmentfrom
vb-kalei:perf/compilation-scoped-attribute-cache

Conversation

@vb-kalei

Copy link
Copy Markdown

This is an AI-assisted performance optimization that speeded up mapper compilation times in our project by 30%.
The documentation of the changes is also AI generated, I hope it would be useful:

Motivation

During compilation, source member matching repeatedly checks AdaptIgnoreAttribute, checks AdaptMemberAttribute for eligibility, and checks AdaptMemberAttribute again for name resolution. Repeated enumeration creates new PropertyModel and FieldModel wrappers, so caching on individual wrappers would not share metadata between scans.

Historical application profiling motivated this change, but is not upstream benchmark evidence. Reported reflection CPU shares changed from 27.37% to 8.90%, with total attributed samples changing from 8,578 to 3,275. Inclusive samples overlap; these are not elapsed timings or call counts.

A separate application's unchanged three-configuration compilation workload reported mean elapsed times of 17.765 seconds with the prototype versus 27.200 seconds with injection bypassed. This involved only two on and three off runs, grouped rather than fully interleaved, Debug builds, fresh processes, no debugger/profiler, and both variants already included the independent hidden-member fix. Cache-off retained cache infrastructure and cleanup overhead. These observations have not been reproduced here and are motivation only, not a general 34.7% improvement claim.

Design and ownership

  • Each root TypeAdapterConfig.CreateMapExpression creates a CompileContext owning an internal AttributeMetadataCache.
  • Inline nested mappings and cloned CompileArgument objects share that context. Separate Map, MapToTarget, and Projection roots have independent contexts. A configuration fork can share metadata within a root without sharing eligibility or naming decisions.
  • The dictionary is keyed by MemberInfo, using its equality semantics, not by name or metadata token alone. Tests cover closed generic, hidden, inherited, and differing reflected-type property identities.
  • Population is lazy: constructing a model does not read metadata. On first lookup the reflection metadata collection is materialized into a read-only snapshot; later wrappers in that context reuse it, including empty results. Attribute types and ordering follow the original metadata API.
  • Public constructors and concrete PropertyModel/FieldModel types are unchanged. Internal overloads optionally receive the cache. There are no new public APIs or configuration switches.
  • Only ValueAccessingStrategy.PropertyOrFieldFn opts source enumeration in. This is the repeated-scan path motivating the change. Destination enumeration, flattening, unflattening, parameter models, and unrelated reflection remain outside the cache.
  • No attribute instances, eligibility decisions, callback results, converted names, or generated expressions are cached. Attribute construction remains per lookup; matching callbacks and their ordering are unchanged by this patch.
  • The root's finally calls Complete on success and failure. It clears entries and permanently disables population. Models retained by callbacks, and contexts retained through CompileException.Argument, subsequently fall back to reflection.
  • The lock protects the metadata dictionary and completion only. It does not make the rest of CompileContext thread-safe. There is no static, thread-static, global, or AsyncLocal cache.

Regression coverage

The application regression tests were migrated to MSTest/Shouldly using the existing friend assembly instead of NUnit and private-field reflection. The new fixture contains 17 executed test cases covering:

  • Property/field metadata reuse across wrappers, context isolation, concrete types, and cloned arguments.
  • Read-only results, reflection ordering, and metadata-free members.
  • Closed generic, hidden, inherited, and reflected-type identity distinctions.
  • Lazy construction, public uncached construction, repeated completion, reflection fallback, and failure/retry during metadata retrieval.
  • Fresh attribute instances on repeated lookup.
  • Successful and exceptional cleanup for Map, MapToTarget, and Projection, while retaining a member, context, and exception.
  • Weak-reference collection of cached keys and snapshots after completion, and collection of contexts/caches while a generated mapping delegate remains usable.
  • AdaptMember renaming, AdaptIgnore, Adapt/MapToTarget behavior, projection, nested mappings, forks, and configuration isolation.
  • Independent concurrent compilations (not concurrent use of one context).

Retention assertions use weak references and forced collection outside non-inlined setup helpers, not allocated-byte counts, private dictionary field names, or timing thresholds. They prove these specific ownership properties, not absence of every possible application memory leak.

Validation

Environment: Windows, .NET SDK 10.0.401; installed .NET runtime families 10, 9, and 8, plus .NET Framework 4.8. Base: development commit 4a8aaa415d434ff86b0182617ad2062ab1794a19.

Check Outcome
Focused fixture, Release: net10.0, net9.0, net8.0, net48 17 passed, 0 failed per framework
Full Mapster.Tests, Release: same four frameworks 396 passed, 6 skipped, 0 failed per framework
Library Release: netstandard2.0, net10.0, net9.0, net8.0 Build passed for all targets
Full solution Release build/tests Build passed; 1,696 passed, 24 skipped, 0 failed across 30 project/framework runs
CI default Debug solution build/tests Build passed; 1,696 passed, 24 skipped, 0 failed across 30 project/framework runs
Visual Studio solution build Passed
Working-tree and upstream diff whitespace, including new files Passed

@DocSvartz

Copy link
Copy Markdown
Contributor

HI, @vb-kalei.
Retarget this PR to the Development branch.

@DocSvartz

Copy link
Copy Markdown
Contributor

This is an improved version #1021 or fully independent PR?

@vb-kalei
vb-kalei changed the base branch from master to development September 18, 2026 14:48
@vb-kalei

Copy link
Copy Markdown
Author

@DocSvartz
Base: yeah, sorry my bad, changed it
No, this is a fully independent PR, and one more is coming (if it's welcome).

@DocSvartz

Copy link
Copy Markdown
Contributor

Ok, I'll need to check that this doesn't conflict with the changes for 10.1. Then it will go into the next version.

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.

2 participants