Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions documentation/Multi-request-Target-Crosstalk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Multi-Request Target Crosstalk

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked "crosstalk" best so far but open to other words here if you've got them!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love this name!


*Arrival-order*–*dependent execution in a shared project instance*

## Summary

MSBuild begins executing a project as soon as it sees one request for it. All requests for the same build request configuration share mutable project state and configuration-scoped target results. Separately arriving requests are therefore not necessarily composable: behavior can depend on which arrives first.

We call this **multi-request target crosstalk**.

## Relevant engine concepts

A `BuildRequest` references a project (plus global properties) and targets.

A `BuildRequestConfiguration` represents the project path, tools version, and global properties. Equivalent requests resolve to the same configuration, which owns the loaded `ProjectInstance` and the current properties and items.

Separately, `ResultsCache` keeps an aggregate `BuildResult` for each `ConfigurationId`. Later requests reuse target results instead of executing the targets again.

Requests for the same configuration therefore share:

1. The mutable state of the project instance.
2. The accumulated history and results of completed targets.

Here, **project instance** means the shared execution context; **configuration** is used when identity or target-result caching matters.

## Definition

**Multi-request target crosstalk** occurs when requests for different targets in the same configuration interact through shared project state or completed target results, making behavior depend on arrival order.

The requests do not need to execute concurrently. Because there is no aggregation of target lists, crosstalk will occur whenever multiple target lists are specified for a configuration.

## Mutable-state example

Suppose a project contains an auxiliary target that modifies an item consumed by `Build`:

```xml
<ItemGroup>
<Compile Include="Normal.cs" />
</ItemGroup>

<Target Name="Auxiliary">
<ItemGroup>
<Compile Include="Additional.cs" />
</ItemGroup>
</Target>

<Target Name="Build" DependsOnTargets="Compile" />

<Target Name="Compile">
<Message Text="Compiling @(Compile)" Importance="High" />
</Target>
```

Two callers request different target sets:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you may want to note a common case is other configurations doing an MSBuild task could be causing these different requests - I know you note the parallel down below but it may not be super obvious that theses other configurations making the requests are the things happening in parallel / non-deterministic


- `Build`.
- `Auxiliary;Build`

If `Build` arrives first, `Compile` omits `Additional.cs`. The later request runs `Auxiliary` but reuses the existing `Compile` and `Build` results.

If `Auxiliary;Build` arrives first, `Auxiliary` adds `Additional.cs` before `Compile` runs. The plain `Build` request then reuses those target results.

The project files and requested targets are identical, but the compiled inputs depend on which request is scheduled first.

Auxiliary targets often are not intended to affect `Build`, but can accidentally modify properties or items it consumes.

## Target-order example

Crosstalk can also change target execution order, and thus build outputs, without directly overwriting state.

Suppose an auxiliary target depends on a target also depended on by `Build`:

```xml
<Target Name="Auxiliary"
DependsOnTargets="ResolveReferences" />

<Target Name="Build"
DependsOnTargets="Prepare;ResolveReferences;Compile" />
```

If `Auxiliary` runs first, it pulls `ResolveReferences` forward; `Build` later reuses that result.

The order differs from a standalone `Build`; here, `Prepare` doesn’t run before `ResolveReferences`.

## Why arrival order varies

Request arrival order can change because of:

- Parallel scheduling and ordinary timing variation.
- Solution or traversal edges competing with project-reference edges.
- Incremental builds changing which projects or targets perform work.

This can make the ordering inconsistent even when the projects are unchanged.

## Relationship to static graph builds

[Static graph builds](specs/static-graph.md#inferring-which-targets-to-run-for-a-project-within-the-graph) address one source of multi-request target crosstalk. Before execution, the graph propagates `ProjectReferenceTargets` mappings, aggregates each node’s target lists, and submits one request with the combined list. With a complete graph and target mappings, separately arriving project-reference requests cannot determine which targets start the project instance.

This eliminates request-arrival crosstalk for targets represented by the graph, but not in every graph build:

- A non-isolated graph build can encounter an `MSBuild` task invocation or target list that was not predicted by the graph and fall back to just-in-time execution, allowing crosstalk. An isolated graph build instead reports the missing result as an error.
- Aggregation removes arrival-order dependence, but the combined target list remains ordered. Static graph preserves order within each propagated list and concatenates lists from different incoming edges. Targets still share mutable state and execute once, so correctness can depend on ordering that is not clearly declared.

A complete static graph therefore prevents this crosstalk but does not make arbitrary target sets freely composable.

## Why the behavior is surprising

Each contributing behavior is established:

- Targets can mutate project properties and items during execution.
- Targets are guaranteed to execute at most once per build for a given configuration.
- Requests can arrive after the configuration starts executing.

The _composition_ is often difficult for project authors and engine contributors to predict or diagnose.
1 change: 1 addition & 0 deletions documentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The folder contains collection of docs and references for MSBuild, detailed info
* [Support for remote host objects](specs/remote-host-object.md)
* [Static graph](specs/static-graph.md)
* [Single project isolated builds: implementation details](specs/single-project-isolated-builds.md)
* [Multi-request target crosstalk](Multi-request-Target-Crosstalk.md)
* [Task isolation](specs/task-isolation-and-dependencies.md)
* [Target maps](wiki/Target-Maps.md)
* [Managing parallelism in MSBuild](specs/resource-management.md)
Expand Down
2 changes: 2 additions & 0 deletions documentation/specs/static-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ Note that graph cycles are disallowed, even if they're using disconnected target

In the classic MSBuild build (i.e. execution of targets), the referencing project chooses which targets to call on the referenced projects and may call into a project multiple times with different target lists and global properties (examples in [project reference protocol](../ProjectReference-Protocol.md)). This is a top-down traversal of dependencies. These calls are made via the [MSBuild task](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-task?view=vs-2019). When building a graph, projects are built before the projects that reference them. This is a bottom-up traversal. Therefore the graph needs to determine the list of targets to execute on a specific project `B` **before** building the referencing projects that reference `B`.

Aggregating those target lists before execution avoids [multi-request target crosstalk](../Multi-request-Target-Crosstalk.md#relationship-to-static-graph-builds) for requests represented by the graph. The linked discussion describes the limits of that protection.

The static graph contains the structural information on which reference projects a referencing project depends on. But it does not contain information on what "depends" means. At build time "depends" means that a referencing evaluated project will call a subset of reference evaluations with some targets. Subset because the static graph is an inferred graph, therefore there are ambiguities during graph construction, and thus it needs to be conservative and represent a superset of the "runtime graph". The "runtime graph" is the actual graph that gets executed during a real build. We cannot know the runtime graph because that would require us to analyze msbuild xml code inside of targets in order to find the `MSBuild task` invocations. This means doing heavy program analysis, like symbolic execution. That would make things very complicated, slower, and would probably introduce even more ambiguity, so a larger superset conservative graph. So we kept it simple and only looked at evaluation time msbuild xml code (i.e. msbuild xml code outside of `<Target>` elements).
To summarize, the static graph does not have insights into the `MSBuild task` callsites. It does not know callsite specific information such as the `Targets="Foo;Bar"` or `Properties="Foo=Bar"` `MSBuild task` attributes.
Since the graph does not have access to MSBuild task callsites, it does not know what targets will get called for a given graph edge.
Expand Down
Loading