Pick up JasperFx 1.27.0 indexed type lookup + trim two SaveChanges allocations#4295
Merged
jeremydmiller merged 1 commit intomasterfrom Apr 27, 2026
Merged
Conversation
…ocations JasperFx 1.27.0 (JasperFx/jasperfx#192) makes CodeGenerationExtensions.FindPreGeneratedType O(1) per call by caching an indexed dictionary per Assembly. Bumps the pin so static-mode cold start stops paying O(ExportedTypes) per ICodeFile.AttachTypes call. Two small Marten-side fixes alongside: * QuickEventAppender previously allocated a new Queue<long> per stream inside the foreach over WorkTracker.Streams, even though the quick-append path never reads from it (only applyRichMetadata dequeues; applyQuick doesn't touch it). Hoist a single throwaway instance out of the loop so bulk appends stop paying one allocation per stream. * DocumentSessionBase.operationDocumentTypes() did Operations().Select(...).Where(...).Distinct(), enumerating Operations() twice and chaining a fresh enumerator stack on every SaveChanges. Replace with a single-pass HashSet<Type> walk. Same observable behavior, fewer intermediate allocations on the hot save path. Companion to the Marten 9.0 cold-start umbrella issue (#4294); the non-breaking subset that didn't have to wait for 9.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
jeremydmiller
added a commit
that referenced
this pull request
Apr 27, 2026
JasperFx.Events 1.30.0 (JasperFx/jasperfx#193) makes ProjectionGraph.DiscoverGeneratedEvolvers cheaper at cold start by filtering framework / library assemblies out before the GetCustomAttributes scan and by caching results across IDocumentStore instances in the same process. Bumps the pin so we benefit immediately. Companion Marten-side optimization in the same PR: pre-populate EventGraph._nameToType from registered event types during Initialize. TypeForDotNetName otherwise falls through Type.GetType(assemblyQualifiedName) on first read of each event type from the database, and that fallback is itself O(loaded-assemblies). Pre-warming both the AssemblyQualifiedName and FullName entries in a single ImHashMap.Swap means the first read of every known event type lands directly in the cache. New types (those discovered lazily via _events.OnMissing) still fall through the existing path. Continues the 8.x cold-start trim begun in #4295. See umbrella issue #4294. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 28, 2026
jeremydmiller
added a commit
that referenced
this pull request
Apr 28, 2026
…ze (#4302) Round 3 of the 8.x cold-start trim begun in #4295/#4296. Two more lazy caches in EventGraph that paid an O(n) linear walk on first lookup of each name; pre-populate them at Initialize from already-known data so the first request of every alias is a hit. * _byEventName is keyed by EventTypeName and OnMissing was a AllEvents().FirstOrDefault(x => x.EventTypeName == name) walk per miss. Add Fill() for every registered mapping in the same loop that already walks _events. First lookup of each alias is now O(1). * _aggregateTypeByName is keyed by aggregate alias and findAggregateType iterated Options.Projections.AllAggregateTypes() on miss. Walk that collection once at Initialize and Fill the entries up front. Same shape as the existing OnMissing logic, just moved off the request path. Behavior unchanged: in both caches, the existing OnMissing/Fill paths remain intact for any name that wasn't registered at Initialize time (rare but possible -- e.g. types added via _events.OnMissing later). Continues the 8.x cold-start trim. The remaining items are 9.0 scope and each tracked separately under the Marten 9.0 milestone. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Apr 28, 2026
Three independent root causes feed the test-flake pattern that's been hitting recent PRs (#4279, #4281, #4292, #4295, #4296, #4302). All three are localized; this PR addresses each. ## Root cause 1: shared static random sequence Target.cs defines `private static readonly Random _random = new Random(67)` that is consumed by ~80 tests across LinqTests + DocumentDbTests. Each Target.Random() / GenerateRandomData() call advances the shared sequence, so a test's effective random data depends on which sibling tests ran before it. xUnit discovery order is mostly stable but NOT guaranteed identical run-to-run, especially across CI workers with different load, .NET TFM combinations, etc. A small order shift consumes a different slice of the sequence and produces different test data — silently flipping assertions that depend on exact counts or distributions. Fix: introduce `Target.ResetRandomSeed(int seed = 67)` so a test that genuinely depends on specific random data can pin the sequence at the start. Remove the readonly modifier on _random to allow rebinding. Updated tests to call ResetRandomSeed(): - Bug_605_unary_expressions_in_where_clause_of_compiled_query (3 facts) - Bug_3337_select_page.try_it_out - query_against_child_collections.buildUpTargetData (covers can_query_on_enum_properties and many more) Also tightened Bug_605's assertion: it was hardcoded to `.ShouldBe(15)` but the real point of the test is "compiled query == inline LINQ query for the same expression"; the page size of 15 is incidental. Compare against expected.Count instead so the test is robust to data variance. ## Root cause 2: DateTimeOffset.UtcNow inside a shared LINQ expression `child_collection_queries.cs:67` was registering this where-clause for the acceptance suite: @where(x => x.Children.Any(c => c.NullableDateOffset <= DateTimeOffset.UtcNow)); That expression runs in BOTH the in-memory LINQ-to-objects "expected" provider AND the LINQ-to-SQL "actual" provider. Each provider evaluates DateTimeOffset.UtcNow at its own moment. Target.NullableDateOffset values are ±60 seconds of "now" from random data; values within microseconds of either provider's "now" can land on opposite sides of <= and disagree. Fix: capture a fixed `asOf = DateTimeOffset.UtcNow.AddDays(1)` in the static ctor and use that as the boundary. The expression now embeds a constant timestamp that both providers see identically. AddDays(1) puts it well beyond the test data range so the predicate is meaningfully true for matching rows. ## Root cause 3: ordering assumptions on server-generated Guids Bug_4282 asserted `ids.ShouldHaveTheSameElementsAs(doc1.Id, doc3.Id)` after `OrderBy(x => x.Id)`. The IDs are server-generated Guids; their sort order does not in general match declaration order (Marten uses sequential Guids in many configs but not always, depending on the StoreOptions in scope and the underlying provider). Switched to set-membership: `Count == 2` plus ShouldContain for each expected id. ## Root cause 4 (defensive): ShouldBeEqualWithDbPrecision tolerance The helper used to round both sides to 100µs with truncation (`Ticks / 1000 * 1000`) and then ShouldBe. The math works in the common case, but the assertion was fragile under loaded-runner clock-comparison edge cases. Switched to a 1ms tolerance check; widely above the worst-case PostgreSQL truncation (9 ticks ≈ 0.9µs) but still tight enough to catch real semantic differences. Also produces a clearer failure message when it does fire. ## Verification Stress-ran the previously-flaky suites locally: 5x consecutive runs of all 178 LinqTests.Bugs tests, no failures. All 123 tests across Bug_605, Bug_4282, Bug_3337, query_against_child_collections, and child_collection_queries pass. Bug_2283 in DocumentDbTests passes. Closes #4310. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 30, 2026
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.
Summary
Non-breaking cold-start + per-
SaveChangesallocation cleanup, carved out of the Marten 9.0 umbrella issue (#4294) so the simpler wins don't have to wait for 9.0.What lands
JasperFxpackage bumped 1.26.0 → 1.27.0, picking up JasperFx/jasperfx#192 (closes JasperFx/jasperfx#189).CodeGenerationExtensions.FindPreGeneratedTypeis now O(1) per call after the first lookup perAssembly, instead of O(ExportedTypes). Static-mode cold start (Marten withTypeLoadMode.Staticand pre-generated code in the entry assembly) stops paying anassembly.ExportedTypes.FirstOrDefaultscan perICodeFile.AttachTypescall.QuickEventAppender: hoist the per-streamQueue<long>allocation out of theforeachoverWorkTracker.Streams. The quick-append path never reads from it (onlyapplyRichMetadatadequeues; the quick variant doesn't touch it), so a single throwaway instance is correct. Bulk-append workloads stop paying one allocation per stream.DocumentSessionBase.operationDocumentTypes(): replaceOperations().Select(...).Where(...).Distinct()with a single-passHashSet<Type>walk. Same observable behavior, fewer intermediate enumerator/list allocations on everySaveChanges.What deliberately did not land
The bigger cold-start items in #4294 — lazy
BuildAllMappings, lazyEventGraph.FeatureSchema.Objects,Static-mode validation skip,IEnumerable<StreamAction>widening — are still 9.0 candidates because they each have semantic-change risk or signature implications. Tracked there.Test plan
Marten+EventSourcingTests)end_to_end_event_capture_and_fetching_the_stream,quick_append_event_capture_and_fetching_the_stream,archiving_events,start_stream_should_enforce_*) — 212 tests, all greenCoreTestssweep — 327 passed, 1 skipped (pre-existing), 0 failed🤖 Generated with Claude Code