Skip to content

Provide a partial evaluation API with a configurable stop pass (props/imports, items, ...) #14288

Description

@ViktorHofer

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:

  1. 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).

  2. 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.

  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions