Summary
Provide a public MSBuild API for partial evaluation that can stop after a chosen evaluation pass - not just a single "properties-only" mode. Callers should be able to say "evaluate up to and including pass N" (e.g. stop after properties/imports, or after items, or after item definitions), producing an object from which the data available at that point can be read, without paying for the later passes.
A single properties-only flag would solve today''s motivating case but is inflexible; different consumers need different cut-off points (see examples below). Designing this as a "stop after pass X" capability future-proofs it.
The motivating scenario: tools that need to read a single property (or a limited slice of evaluated state) from a project - or from every project in a solution - currently pay for a full evaluation. A concrete example is the .NET SDK CLI, which pre-evaluates project(s) before dotnet pack/dotnet publish purely to read PackRelease/PublishRelease and decide whether to inject Configuration=Release (see dotnet/sdk#55193, and ReleasePropertyProjectLocator). For a solution it evaluates every project.
Data: how much would this save?
Per-pass breakdown for a single evaluation of a minimal SDK project (dotnet msbuild -profileevaluation, local build; percentages are the point, absolute ms are inflated by a Debug engine):
| Pass |
Inclusive |
% |
Cumulative if you stop here |
| 0 - Initial properties |
40 ms |
2.3% |
2.3% |
| 1 - Properties + imports |
1102 ms |
62.8% |
65.1% |
| 2 - Item definitions |
5 ms |
0.3% |
65.4% |
| 3 - Items (globbing) |
176 ms |
10.0% |
75.4% |
| 3.1 - Lazy items |
206 ms |
11.8% |
87.2% |
| 4 - UsingTasks |
57 ms |
3.2% |
90.4% |
| 5 - Targets (registration) |
160 ms |
9.1% |
100% |
| Total |
1756 ms |
100% |
|
So the amount saved depends on where you stop:
- Stop after pass 1 (props/imports): skip passes 2-5 = ~34% saved (items/globbing ~22%, targets ~9%, usingtasks ~3%). Enough for property-only reads.
- Stop after pass 3.1 (items): skip UsingTasks + Targets = ~12% saved. Enough for consumers that need items but never build (e.g. design-time item enumeration, dependency crawling).
- Stop after pass 4 (UsingTasks): skip target registration = ~9%.
On file-heavy projects the item/globbing share (passes 3/3.1) grows with source-file count (default **/*.cs enumeration), so stopping before items saves proportionally more.
Correctness (per cut-off point)
Each pass only produces state that later passes may consume, so stopping after pass N is safe as long as the caller only reads state produced by passes 0..N:
- After pass 1: all properties have final values. Properties cannot depend on items, so
PackRelease, PublishRelease, TargetFramework(s), OutputType, etc. are final. Reading items/targets is invalid (they aren''t populated).
- After pass 3/3.1: properties + items are final; targets/usingtasks not yet registered.
- This is distinct from skipping imports (which would produce wrong values). The proposal always runs all imports/properties, then optionally stops before the later passes.
Caveat to document across all modes: any value computed inside a target (not during evaluation) is not observable via evaluation at all - full evaluation wouldn''t see it either, so partial evaluation is not a regression.
Proposed shape (sketch, open to design)
Prefer a flexible cut-off over a single boolean. Options, roughly in order of surface-area:
-
Explicit stop-point enum. Reuse/extend the existing EvaluationPass concept (src/Framework/Profiler/EvaluationLocation.cs) as a public "evaluate up to" selector, threaded through ProjectLoadSettings/an evaluation option, e.g.:
// conceptual
var options = new ProjectOptions {
EvaluateUpToPass = EvaluationPass.Properties, // or Items, ItemDefinitionGroups, ...
EvaluationContext = sharedContext,
};
The resulting object exposes state through the chosen pass; accessing state from a later pass throws (mirrors how DoNotEvaluateElementsWithFalseCondition already makes certain members throw).
-
A couple of named ProjectLoadSettings flags for the common cut-offs (e.g. StopAfterImports, StopAfterItems) if a full "up to pass X" selector is deemed too broad initially. Less flexible; can be layered on later.
-
Dedicated lightweight helpers for the hottest case, e.g. ProjectInstance.EvaluatePropertiesOnly(path, globalProperties, toolsVersion, evaluationContext), implemented on top of the general mechanism.
Whatever the surface, the goal is that the underlying engine supports an arbitrary stop pass and the public API exposes at least the useful ones.
Implementation note: Evaluator.Evaluate already runs the passes sequentially (src/Build/Evaluation/Evaluator.cs, EvaluatePass0/Pass1 ... Targets), so this is primarily a matter of (a) taking a "stop after pass N" parameter, (b) returning after that pass, and (c) marking the resulting object so that reads of not-yet-evaluated state fail fast.
Relationship to EvaluationContext (complementary, not a substitute)
Ideally consumers use both.
Related
Summary
Provide a public MSBuild API for partial evaluation that can stop after a chosen evaluation pass - not just a single "properties-only" mode. Callers should be able to say "evaluate up to and including pass N" (e.g. stop after properties/imports, or after items, or after item definitions), producing an object from which the data available at that point can be read, without paying for the later passes.
A single properties-only flag would solve today''s motivating case but is inflexible; different consumers need different cut-off points (see examples below). Designing this as a "stop after pass X" capability future-proofs it.
The motivating scenario: tools that need to read a single property (or a limited slice of evaluated state) from a project - or from every project in a solution - currently pay for a full evaluation. A concrete example is the .NET SDK CLI, which pre-evaluates project(s) before
dotnet pack/dotnet publishpurely to readPackRelease/PublishReleaseand decide whether to injectConfiguration=Release(see dotnet/sdk#55193, andReleasePropertyProjectLocator). For a solution it evaluates every project.Data: how much would this save?
Per-pass breakdown for a single evaluation of a minimal SDK project (
dotnet msbuild -profileevaluation, local build; percentages are the point, absolute ms are inflated by a Debug engine):So the amount saved depends on where you stop:
On file-heavy projects the item/globbing share (passes 3/3.1) grows with source-file count (default
**/*.csenumeration), so stopping before items saves proportionally more.Correctness (per cut-off point)
Each pass only produces state that later passes may consume, so stopping after pass N is safe as long as the caller only reads state produced by passes 0..N:
PackRelease,PublishRelease,TargetFramework(s),OutputType, etc. are final. Reading items/targets is invalid (they aren''t populated).Caveat to document across all modes: any value computed inside a target (not during evaluation) is not observable via evaluation at all - full evaluation wouldn''t see it either, so partial evaluation is not a regression.
Proposed shape (sketch, open to design)
Prefer a flexible cut-off over a single boolean. Options, roughly in order of surface-area:
Explicit stop-point enum. Reuse/extend the existing
EvaluationPassconcept (src/Framework/Profiler/EvaluationLocation.cs) as a public "evaluate up to" selector, threaded throughProjectLoadSettings/an evaluation option, e.g.:The resulting object exposes state through the chosen pass; accessing state from a later pass throws (mirrors how
DoNotEvaluateElementsWithFalseConditionalready makes certain members throw).A couple of named
ProjectLoadSettingsflags for the common cut-offs (e.g.StopAfterImports,StopAfterItems) if a full "up to pass X" selector is deemed too broad initially. Less flexible; can be layered on later.Dedicated lightweight helpers for the hottest case, e.g.
ProjectInstance.EvaluatePropertiesOnly(path, globalProperties, toolsVersion, evaluationContext), implemented on top of the general mechanism.Whatever the surface, the goal is that the underlying engine supports an arbitrary stop pass and the public API exposes at least the useful ones.
Implementation note:
Evaluator.Evaluatealready runs the passes sequentially (src/Build/Evaluation/Evaluator.cs,EvaluatePass0/Pass1...Targets), so this is primarily a matter of (a) taking a "stop after pass N" parameter, (b) returning after that pass, and (c) marking the resulting object so that reads of not-yet-evaluated state fail fast.Relationship to EvaluationContext (complementary, not a substitute)
EvaluationContext(SharingPolicy.Shared/SharedSDKCache) attacks the 62.8% pass-1 cost across projects 2..N by caching SDK resolution and import file I/O. Best lever for the multi-project/solution case. (Filed for the SDK in Use a shared EvaluationContext for ReleasePropertyProjectLocator pre-evaluation (pack/publish) sdk#55193.)Ideally consumers use both.
Related