Fill holes in BuildEventContext construction and evaluation ID propagation#12946
Fill holes in BuildEventContext construction and evaluation ID propagation#12946
Conversation
| int nodeId = reader.ReadInt32(); | ||
| int projectContextId = reader.ReadInt32(); | ||
| int targetId = reader.ReadInt32(); | ||
| int taskId = reader.ReadInt32(); |
There was a problem hiding this comment.
@rainersigwald looking at this, it seems to me that we should be sending evalIds for this, but we're not and it's not versioned :(
There was a problem hiding this comment.
Added this in a recent commit, so this hole should be plugged.
| <?xml version="1.0" encoding="utf-8"?> | ||
| <!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids --> | ||
| <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> | ||
| <Suppression> |
There was a problem hiding this comment.
We'll probably have to add all of these back, but I like having the file here to call out the changes for discussion
There was a problem hiding this comment.
- would it make sense to split from the actual fixes in this PR the Builder pattern new API? this pr is huge 😵
- perhaps we can add to BannedAPIAnalyzer config the current errorprone APIs so we avoid this footgun without a breaking change for customers?
There was a problem hiding this comment.
this pr is huge
I mean, it's a refactoring so I kinda expect that. Logically there are only two actual changes
- the introduction of the new API
- consuming the new API
I added review comments to help guide the way on initial submission, but I can definitely do a rebase pass and split the work into buckets for easier commit-by-commit review.
| projectBuildEventContext.ProjectContextId, | ||
| NextTargetId, | ||
| BuildEventContext.InvalidTaskId); | ||
| BuildEventContext targetBuildEventContext = projectBuildEventContext.WithTargetId(NextTargetId); |
There was a problem hiding this comment.
Here's an example of losing an evalId on context creation - fixed with the new API
| targetBuildEventContext.ProjectContextId, | ||
| targetBuildEventContext.TargetId, | ||
| NextTaskId); | ||
| BuildEventContext taskBuildEventContext = targetBuildEventContext.WithTaskId(NextTaskId); |
There was a problem hiding this comment.
Here's another example of losing an evalId on context creation - fixed with the new API
|
Really worried about new allocations caused by this pattern. This is allocated hundreds of millions of times, so each With call is an allocation of a rather chonky object |
There was a problem hiding this comment.
Pull request overview
This PR refactors BuildEventContext construction to use a builder pattern approach to prevent evaluation IDs and other context values from being accidentally dropped during construction. All BuildEventContext instances are now created via CreateInitial(submissionId, nodeId) followed by fluent WithXxx() methods that preserve all existing IDs while updating specific ones.
Key Changes:
- Introduces
BuildEventContextBuilder(a ref struct) that enables efficient, stack-allocated context construction - Makes the full constructor internal, forcing external code to use factory methods
- Adds compatibility suppressions for the deprecated public constructors
- Updates all call sites across production and test code to use the new builder pattern
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| BuildEventContext.cs | Core refactoring: added builder pattern with CreateInitial and WithXxx methods, made constructor internal |
| CompatibilitySuppressions.xml | Added suppressions for CP0002/CP0009 warnings for deprecated constructors across all target frameworks |
| BinaryReaderExtensions.cs, BinaryTranslator.cs, BuildEventArgs.cs, ProjectStartedEventArgs.cs, BuildEventArgsReader.cs | Updated deserialization to use new builder pattern |
| ProjectInstance.cs, ProjectCollection.cs, Project.cs | Updated initialization contexts |
| BuildManager.cs, RequestBuilder.cs, Scheduler.cs, LoggingService methods | Updated context creation in build orchestration |
| BuildCheckBuildEventHandler.cs | Simplified to use BuildEventContext.Invalid |
| Multiple test files | Updated test BuildEventContext construction throughout test suite |
baronfel
left a comment
There was a problem hiding this comment.
Really worried about new allocations caused by this pattern. This is allocated hundreds of millions of times, so each With call is an allocation of a rather chonky object
Yup, good call-out - I made the builders a ref-struct pattern with explicit 'commit' to manage this better.
|
Current breaks are coming from my attempt to plug the hole where a project is served from cache - in this case the eval Id is set as |
|
Things to check on:
|
|
Ok, the recent changes have fixed both the issues in #12953 as well as making TL no longer blow up. This is looking pretty good to me, at least for deeper review and feedback. |
04261b5 to
b2c4bf0
Compare
807ae4f to
a8e990d
Compare
e9d699f to
219756f
Compare
2952b34 to
bbfe6a3
Compare
|
/azp run msbuild-pr |
|
Azure Pipelines successfully started running 1 pipeline(s). |
…e the project lived on when sending cached results
…cially for cached project contexts
…cache-based projectstarted messages from the scheduler node. Before we were incorrectly saying they came from the remote node.
f5ce6bd to
b58e86f
Compare
|
Next steps:
|
Two issues fixed: 1. Cache-hit hang (DisablingCacheResetKeepsInstance and similar): CreateProjectStartedForCachedProject derived the event BuildEventContext from parentBuildEventContext (which is Invalid for top-level requests), losing the submissionId. The OnProjectFinished handler couldn't find the submission to call CompleteLogging(), so the CompletionEvent was never signaled. Fix: derive from validatedRemoteNodeEvaluationBuildContext which preserves submissionId, nodeId, and evaluationId. 2. RequestBuilder_Tests failures (TestSimpleBuildRequest etc.): The new HandleProjectStarted method calls GetWarningsAsErrors/Messages/ NotAsErrors on ILoggingService, but MockLoggingService threw NotImplementedException. Fix: return empty collections. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert new BuildEventContext constructor calls from main to use the builder pattern (CreateForSubmission, CreateInitial, submission.BuildEventContext). Add back CreateErrorLoggingContext helper lost in merge. Fix Telemetry_Tests to use builder pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
24-dimension expert MSBuild code review of PR #12946. Flags 3 blocking issues, 2 major, and 4 moderate findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CreateForLocalBuild derived the child project's BuildEventContext from parentBuildEventContext, inheriting the parent's evaluationId and nodeId. The old code explicitly used the child's evaluationId (from configuration) and the current node's nodeId. This broke the console logger's propertyOutputMap lookup via GetEvaluationKey(nodeId, -evaluationId), causing ProjectConfigurationDescription items to not display in error/warning messages (e.g. [project.proj::Number=1]). Fix: override evaluationId and nodeId on the child's BuildEventContext after creation, using the child's ProjectEvaluationId and the current node's nodeId. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The old CreateProjectStarted had a second pass that applied PropertiesToSerialize filtering (MsBuildForwardPropertiesFromChild) even on OOP nodes where RunningOnRemoteNode=true would normally skip all properties. The new GetProjectProperties method was missing this fallback, causing forwarded properties to be empty. Fix: check PropertiesToSerialize before the RunningOnRemoteNode early return, matching the old behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-add the 4 original public constructors that were made internal: - (int nodeId, int targetId, int projectContextId, int taskId) - (int nodeId, int projectInstanceId, int projectContextId, int targetId, int taskId) - (int submissionId, int nodeId, int projectInstanceId, int projectContextId, int targetId, int taskId) - (int submissionId, int nodeId, int evaluationId, int projectInstanceId, int projectContextId, int targetId, int taskId) These are kept public for backward compatibility (1P and 3P consumers may create these types), but are discouraged: - [EditorBrowsable(Never)] hides them from IntelliSense - XML doc comments direct users to the new builder pattern - BannedApiAnalyzer (RS0030) prevents internal MSBuild code from using them via eng/BannedSymbols.txt wired through Directory.Build.props - Legitimate internal uses (#pragma warning disable RS0030) are limited to: constructor chaining, BuildEventContext.Invalid, BuildEventContextBuilder.Build(), and binary deserialization Also removes the 25 CompatibilitySuppressions.xml entries (CP0002/CP0009) that were added for the removed constructors, and removes review.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
9b07aab to
eae6a86
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| /// <summary> | ||
| /// Clones the build result (the resultsByTarget field is only a shallow copy). | ||
| /// </summary> | ||
| internal BuildResult Clone() |
There was a problem hiding this comment.
Should we also pass evaluation id here?
| /// </summary> | ||
| /// <param name="existingResults">The existing results.</param> | ||
| /// <param name="targetNames">The target names whose results we will take from the existing results, if they exist.</param> | ||
| internal BuildResult(BuildResult existingResults, string[] targetNames) |
There was a problem hiding this comment.
Should we also pass evaluation id here?
| /// <summary> | ||
| /// Constructor which allows reporting results for a different nodeRequestId | ||
| /// </summary> | ||
| internal BuildResult(BuildResult result, int nodeRequestId) |
There was a problem hiding this comment.
Should we also pass evaluation id here?
| _baseOverallResult = result.OverallResult == BuildResultCode.Success; | ||
| } | ||
|
|
||
| internal BuildResult(BuildResult result, int submissionId, int configurationId, int requestId, int parentRequestId, int nodeRequestId) |
There was a problem hiding this comment.
Should we also pass evaluation id here?
…ect eval ID (#13480) Fixes #13095 ## Context These metaproj files are never evaluated — they have `EvaluationId = -1`. The TerminalLogger's `Debug.Assert` assumes every `ProjectStarted` has a prior `ProjectEvaluationFinished`, which fails for metaproj files and crashes the build. Additionally, a pre-existing bug (#12953) causes real projects to lose their evaluation IDs when their `ProjectInstance` is cached (released from memory), and when `BuildResult` packets are sent from worker nodes to the central node without carrying the eval ID. ## Changes Made **TerminalLogger metaproj fix:** - `TerminalLogger.cs`: Assert "EvalProjectInfo should have been captured before ProjectStarted"); will not fail now for metaproj files **Evaluation ID fixes (adapted from PR #12946):** - `BuildRequestConfiguration.cs`: Added `_projectEvaluationId` field that persists through caching + IPC serialization in both `Translate()` and `TranslateForFutureUse()` - `NodeLoggingContext.cs`: Use `config.ProjectEvaluationId` instead of `config.Project.EvaluationId` (which is inaccessible when cached) - `BuildResult.cs`: Added `_evaluationId` field with version 2 serialization - `RequestBuilder.cs`: Populate `result.EvaluationId` at success, exception, and abort paths - `BuildManager.cs`: Update `config.ProjectEvaluationId` from worker node results ## Testing **Unit tests** - `BuildRequestConfiguration_Tests` - `BuildResult_Tests` **Integration testing:** - Verified by building Roslyn and Aspire with Debug bootstrap MSBuild with and without /mt. ## Notes - The evaluation ID fixes are adapted from PR #12946.
Conflicts resolved: - NodeLoggingContext.cs: keep PR's CreateForCacheBuild factory - RequestBuilder.cs: trivial comment/variable name differences (kept upstream) - BuildRequestConfiguration.cs: deduplicate _projectEvaluationId serialization (both PR and upstream added it at different positions; keep upstream's position) - BuildResult.cs: deduplicate _evaluationId field and EvaluationId property (both PR and upstream added them; remove upstream's duplicate) - BuildResult_Tests.cs: convert new upstream test to builder pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes #12953 and #12998
Summary
This PR addresses two critical issues with evaluation ID tracking in MSBuild:
BuildEventContext Construction: Refactors
BuildEventContextconstruction throughout MSBuild to prevent evaluation IDs and other critical context data from being accidentally dropped during logging operations.Distributed Build Evaluation ID Propagation: Implements proper evaluation ID propagation from worker nodes to the central BuildManager in distributed build scenarios.
The core issues were that MSBuild's logging infrastructure used multiple constructor overloads that made it easy to accidentally omit important ID values, and that
BuildResultobjects weren't preserving evaluation context when sent between nodes in distributed builds.Problem Statement
BuildEventContext Construction Issues
MSBuild's
BuildEventContextclass had multiple public constructors that made it easy to accidentally drop ID values when creating derived contexts. This was particularly problematic for:Distributed Build Evaluation ID Loss
In MSBuild's distributed build architecture, worker nodes evaluate and build projects independently, but the central BuildManager wasn't receiving complete evaluation context. The issues were:
BuildResultobjects weren't serializing evaluation IDs during node communicationSolution Overview
This PR introduces two complementary solutions:
BuildEventContext Builder Pattern
Implements a builder pattern approach using:
BuildEventContext.CreateInitial()- Factory method for root contextsWithXxx()methods - For creating derived contexts that preserve all existing IDsBuildEventContextBuilderref struct - Zero-allocation builder for efficient context constructionDistributed Build Evaluation ID Propagation
Ensures evaluation IDs flow correctly from worker nodes to central BuildManager:
_evaluationIdfield to theTranslate()method for node packet communicationBuildResult.EvaluationIdfrom configuration after builds completeKey Architecture Changes
1. Builder Pattern Implementation
Before (Error-Prone):
After (Safe & Fluent):
2. Zero-Allocation Builder Pattern
The new
BuildEventContextBuilderis a ref struct that operates entirely on the stack:Performance Impact:
Build())3. Factory Method for Root Contexts
4. Distributed Build Evaluation ID Flow
Before (Incomplete Flow):
After (Complete Flow):
Specific Bug Fixes with Examples
BuildEventContext Construction Issues
Bug 1: Evaluation ID Loss in Event Deserialization
File:
ProjectStartedEventArgs.csProblem: When deserializing project events from binary logs, evaluation IDs were being dropped
Before (Lines 415-438):
After (Lines 431-450):
Impact: Binary log files now preserve complete evaluation context across all supported versions.
Bug 2: Null Reference in BuildRequestConfiguration
File:
BuildRequestConfiguration.csProblem: Accessing
ProjectEvaluationIdcould cause null reference exceptions when project was cachedBefore:
After (Line 303):
Impact: Eliminates null reference exceptions and ensures evaluation ID is always available, even when project instances are cached and released.
Bug 3: Inconsistent Context Creation in Logging Services
File:
LoggingServiceLogMethods.csProblem: Direct constructor usage led to inconsistent ID propagation
Before (Multiple locations):
After:
Bug 4: Test Suite Context Inconsistencies
Problem: Test code used inconsistent constructor patterns, making it hard to verify correct behavior
Before (BuildRequest_Tests.cs:312):
After:
Distributed Build Evaluation ID Issues
Fix 1: BuildResult Serialization Enhancement
File:
BuildResult.cs(Line ~715)Problem: Evaluation IDs were not being serialized when BuildResult objects were sent between nodes
Before:
After:
Impact: BuildResult packets sent between nodes now preserve evaluation ID context.
Fix 2: Worker Node Result Population
File:
RequestBuilder.cs(Line ~1213)Problem: Worker nodes weren't populating evaluation IDs in BuildResult objects before sending to central node
Before:
After:
Impact: All successful builds now send complete evaluation context to the central BuildManager.
Fix 3: Exception and Abort Case Handling
Files:
RequestBuilder.cs(Lines ~870, ~1026)Problem: BuildResults created for exception and abort scenarios didn't include evaluation IDs
Before:
After:
Impact: Even failed and cancelled builds maintain evaluation traceability in distributed scenarios.
Compatibility & Migration
API Compatibility
internalBuildEventContextBuildertoBuildEventContextfor seamless usageMigration Path
CreateInitial()andWithXxx()methods exclusivelyPerformance Improvements
Memory Allocation Reduction
Build())ref struct) - zero heap impactExample Performance Impact
Testing & Validation
Automated Testing
Manual Validation
Files Changed (Summary)
BuildEventContext Builder Pattern Changes
Core Framework (3 files)
BuildEventContext.cs- New builder pattern implementationCompatibilitySuppressions.xml- API compatibility suppressionsProjectStartedEventArgs.cs- Fixed evaluation ID deserializationBuild Engine (15+ files)
Test Suite (30+ files)
Distributed Build Evaluation ID Propagation Changes
Core Build Components (2 files)
BuildResult.cs- Added evaluation ID serialization toTranslate()methodRequestBuilder.cs- Population of evaluation IDs in all BuildResult creation scenariosDistributed Build Flow
BuildResult.EvaluationIdfromBuildRequestConfiguration.ProjectEvaluationIdBuildManager.HandleResult()updates configurations with evaluation IDs from worker resultsFuture Benefits
BuildEventContext Improvements
Distributed Build Reliability
Combined Impact
This PR establishes a foundation for reliable, high-performance context tracking throughout MSBuild's infrastructure. It ensures that critical debugging information is never accidentally lost during build operations, whether in local logging scenarios or complex distributed builds. The combination of improved BuildEventContext construction and proper evaluation ID propagation creates a robust system for tracking project evaluation context across all MSBuild scenarios.