perf(stages)!: build the level chain once at freeze - #4
Merged
Conversation
The root props file imports it when it exists, and .gitignore keeps it out of the repo. It carries the InternalsVisibleTo the benchmarks need to reach ChainBuilder and LevelEntry<TResponse>, which the packed assemblies should not name.
A leg whose runner fails to load reports no tests and exits zero, so the run reads as green with a target left unexercised. tests.runsettings sets TreatNoTestsAsError, and the test props point every test project at it.
A level was built per next call, so a dispatch through N stages allocated N objects and a retry allocated the levels it re-entered. A level holds nothing belonging to one call, so building it per call, or per dispatch, buys nothing. Each plan now builds its chain of LevelEntry delegates when the dispatch map freezes, bottom-up from the handler. The request, the provider, and the token reach a level as arguments, so one chain serves every dispatch of its request type. Five stages allocate what no stages allocate, and a repeated next call allocates nothing of RequestFlow's. Stage instances are still the container's, on the lifetime they were registered with. Resolution is unchanged: a level resolves its stage on every entry, and the bottom level the handler. So is the shape a void stage runs under, which the freeze still records per level. NoResultBridge now recovers the Task<NoResult> a void stage hands back from its own next call, so a running pass-through level crosses the bridge free instead of wrapping a task per level. Also documents what a registration made after BuildServiceProvider does. The registry is per collection and the map freezes off it, so the map gets a plan the provider has no descriptor for, and the dispatch fails on the container's own "No service for type". docs/lifetimes.md says to register before the provider is built. BREAKING CHANGE: IContinuation<TResponse> and IContinuation are gone. A stage takes Continuation<TResponse> or Continuation, readonly structs over the frozen chain. The calls on next do not change, so a stage migrates by editing one parameter type. Its tests take more: a struct cannot be substituted, so build a real one with Continuation<TResponse>.Over(rest) or Continuation.Over(rest), which runs a delegate where the levels below would be. Invoking the default value of either struct throws InvalidOperationException, since it has no chain behind it.
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.
A level used to be an object built per
nextcall: N stages meant N allocations, and a retry re-allocated everything below it. A level holds nothing belonging to one call, so it never needed building per call.Each plan now builds a chain of
LevelEntry<TResponse>delegates once, when the dispatch map freezes. The request, the provider, and the token reach a level as arguments, so one chain serves every dispatch of its request type.nextcall allocates nothing of RequestFlow's.NoResultBridgerecovers theTask<NoResult>a void stage hands back from its ownnextcall, so a running pass-through level crosses the bridge free.docs/lifetimes.mdcovers registering afterBuildServiceProvider: the map gets a plan the provider has no descriptor for, and the dispatch fails on the container's own "No service for type".Two commits ride along: an ignored
Directory.Local.propsfor per-machine settings, which is where the benchmarks'InternalsVisibleTolives, andTreatNoTestsAsError, so a test leg whose runner fails to load stops exiting green.BREAKING CHANGE:
IContinuation<TResponse>andIContinuationare gone. A stage takesContinuation<TResponse>orContinuation, bothreadonly structs over the frozen chain. The calls onnextdo not change, so migrating a stage means editing one parameter type. Its tests take more: a struct cannot be substituted, so build a real one withContinuation<TResponse>.Over(rest). Invoking a defaultContinuationthrowsInvalidOperationException.