Separate the data-tracking parts of Terminal Logger into a reusable base#13591
Separate the data-tracking parts of Terminal Logger into a reusable base#13591
Conversation
…st cares about rendering
There was a problem hiding this comment.
Pull request overview
Refactors TerminalLogger by extracting build/project/evaluation tracking into a reusable generic base class, with the goal of enabling additional logger implementations to reuse the same lifecycle/state association logic.
Changes:
- Introduces
ProjectTrackingLoggerBase<...>to centralize event subscriptions and tracking of eval/project/build state. - Updates
TerminalLoggerto derive from the new base and moves build-scoped counters/state intoTerminalBuildData. - Promotes several TerminalLogger-related helper types (and
TerminalColor) frominternaltopublicto support reuse.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Framework/Logging/TerminalColor.cs | Makes TerminalColor public (now part of Framework API surface). |
| src/Build/Microsoft.Build.csproj | Adds ProjectTrackingLoggerBase.cs to the Build project compilation items. |
| src/Build/Logging/ProjectTrackingLoggerBase.cs | New reusable base class to track eval/project/build state and route events to virtual hooks. |
| src/Build/Logging/TerminalLogger/TerminalLogger.cs | Switches TerminalLogger to use the new base and build-scoped TerminalBuildData. |
| src/Build/Logging/TerminalLogger/TerminalBuildData.cs | New build-scoped state container (start time, restore state, totals). |
| src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs | Promotes and simplifies project/eval info types for reuse. |
| src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs | Promotes node status type to public for reuse by other renderers. |
| src/Build/Logging/TerminalLogger/TerminalMessageSeverity.cs | Makes severity enum public. |
| src/Build/Logging/TerminalLogger/TerminalBuildMessage.cs | Makes build message struct public. |
| src/Build/Logging/TerminalLogger/StopwatchAbstraction.cs | Makes stopwatch abstraction public. |
Comments suppressed due to low confidence (2)
src/Build/Logging/TerminalLogger/StopwatchAbstraction.cs:12
StopwatchAbstractionwas madepublicbut has no XML documentation. If it’s intended as part of a reusable logger programming model, it should be documented (purpose, threading expectations, howElapsedSecondsis defined). Otherwise, consider keeping itinternalto avoid committing to this abstraction as a supported API.
public abstract class StopwatchAbstraction
{
public abstract void Start();
public abstract void Stop();
public abstract double ElapsedSeconds { get; }
}
src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs:19
TerminalNodeStatusis nowpublic, but it overridesGetHashCode()by throwingNotImplementedException. This is a footgun for any consumer that uses the type in hashed collections or callsGetHashCodeimplicitly. Implement a stable hash code consistent with the equality logic (or remove the override if it’s not intended to be used).
public class TerminalNodeStatus
{
public string Project { get; }
public string? TargetFramework { get; }
public string? RuntimeIdentifier { get; }
public TerminalColor TargetPrefixColor { get; } = TerminalColor.Default;
Public API Surface ReviewThis PR makes 9 previously-internal types public across Types being made public
Key questions
|
There was a problem hiding this comment.
Review Summary
This is a well-motivated refactoring — extracting build state tracking into a reusable base class is a great direction for making logger authoring easier. However, the current implementation has several behavioral regressions that need to be fixed before merging.
🔴 Bugs (3)
-
Quiet-mode regression in
OnProjectFinished(line 712-716): Early return atVerbosity <= Quietskips error/warning count accumulation,Succeededassignment, andStopwatch.Stop(). The old code always accumulated counts even in quiet mode. -
Quiet-mode regression in
OnWarningRaised/OnErrorRaised(lines 1194-1196, 1276): NewVerbosity > Quietguard prevents per-project warning/error accumulation in quiet mode. Old code always accumulated, contradicting the existing comment "In quiet mode, we still accumulate." -
Missing
RestoreContextguard inOnTargetStarted(line 943): Old code was gated by_restoreContext is null— new code processes ALL targets during restore, causing incorrect node status updates.
🟡 Design Issues (3)
-
Restore-phase project tracking change: The base class unconditionally creates project data, but the old TL only created entries when
_restoreContext is null. This cascading change affects every downstream callback during restore. -
TNodeDataunused: The type parameter exists in the public signature but the base class never references it. Once shipped, it can't be removed. -
YieldNodeis accidentallypublicon asealedclass — should beprivate.
🟡 API Surface (see separate comment)
9 internal types made public with Terminal-specific names for a "generic" base class. No test coverage for behavioral equivalence.
Recommendation
Fix the three bugs first (ideally with tests that cover quiet mode and restore scenarios). Then consider whether the API shape is ready for public consumption or whether the types should remain internal until the design stabilizes.
Generated by Expert Code Review (on open) for issue #13591 · ● 12.3M
|
/review |
|
✅ Expert Code Review (command) completed successfully! Expert reviewer sub-agent launched for PR #13591. Running 24-dimension code review in parallel batches. Will post inline review comments, design-level concerns, and final verdict via safe-output tools once complete. |
There was a problem hiding this comment.
24-Dimension Code Review — PR #13591
Summary: The refactoring direction is sound — extracting build-state tracking into a reusable base is a good idea for the logger ecosystem. The behavioral refactoring of TerminalLogger onto the new base correctly preserves the original restore-tracking, node-yielding, and project-lifecycle semantics. However, there are API surface concerns that should be resolved before shipping since this introduces permanent public API.
Findings
| # | Dimension | Severity | Finding |
|---|---|---|---|
| 1 | API Surface | 🔴 High | TNodeData type parameter is unused — phantom public API that's locked once shipped |
| 2 | API Surface | 🟡 Medium | TerminalBuildData has all-public mutable setters; should restrict to internal set |
| 3 | Design | 🟡 Medium | IncludeTaskInputs()/IncludeEvaluationPropertiesAndItems() unconditionally called — taxes lightweight subclasses |
| 4 | Concurrency | 🟡 Medium | Threading contract not documented — critical for public API since Dictionary is used unsynchronized |
| 5 | Performance | 🟢 Low | .ToArray() on BuildFinished allocates unnecessarily |
| 6 | Formatting | 🟢 Low | Indentation inconsistency in TryGetEvalDataForProject |
Clean Dimensions (18/24)
Backwards Compat ✅ | ChangeWave ✅ | Allocation (except #5) ✅ | Test Coverage ✅ (existing TL tests cover refactored behavior) | Error Messages ✅ | Logging ✅ | Target Authoring ✅ | Cross-Platform ✅ | Code Simplification ✅ | Naming ✅ | SDK Integration ✅ | Evaluation Model ✅ | Correctness ✅ | Dependency Mgmt ✅ | Security ✅ | Build Infra ✅ | Binary Log Compat ✅ | Nullable ✅
Notes
- The TerminalLogger refactoring faithfully preserves restore lifecycle semantics (verified by tracing the ProjectStarted → CreateProjectData → OnProjectStarted → OnProjectFinished flow against the original handler).
ProjectContextandEvalContextcorrectly remaininternal— good API discipline.- No
PublicAPI.Unshipped.txtfiles exist in this repo, so that's not a concern here (the project usesCompatibilitySuppressions.xmlfor API compat instead).
Generated by Expert Code Review (command) for issue #13591 · ● 7.4M
Co-authored-by: Copilot <copilot@github.com>
6dabe8a to
26ee836
Compare
|
/review |
|
❌ Expert Code Review (command) failed. Please review the logs for details. Performance & Allocation Awareness: LGTM Reviewed
No performance issues found. |
Correctness & Edge Cases Review — LGTM (with one minor defensive note)I evaluated the five potential issues in ✅ Issue 1 & 2: Generic pattern matching — Correct
✅ Issue 3:
|
📋 Design Discussion: Consider simplifying the generic API before mergingThe 4-parameter generic signature
This isn't blocking since the PR is described as low-priority exploration, but these design decisions will be permanent once shipped. Worth iterating on the API shape before committing to it.
|
There was a problem hiding this comment.
Test Coverage & Completeness: ISSUE (MAJOR)
A 604-line public abstract base class (ProjectTrackingLoggerBase<,,, >) with complex state management, concurrent data access patterns, and 4 abstract + 11 virtual methods ships with zero dedicated tests.
The existing TerminalLogger_Tests provide indirect coverage of one specific parameterization but do not exercise the base class contract independently. Since this type is explicitly designed as a public extensibility point for third-party loggers, its contract needs direct test coverage — particularly for null-context guards, the UsesPerNodeData opt-in toggle, replay-mode array resizing, and state cleanup between builds.
See inline comment for specific untested paths and recommended test approach.
Generated by Expert Code Review (command) for issue #13591 · ● 59.8M
Co-authored-by: Copilot <copilot@github.com>
This makes it easier to implement other Loggers - you just need to pick out what kinds of data you need from evaluation/projects, and then render warnings, errors, etc however you want during certain lifecycle events. The thought is that all of the data required to track/associate/trigger the lifecycle warnings is kept by the 'tracker', so your use-case-specific logger can mostly ignore that.
This is low-priority ATM, but something that may be useful for us as we think about how Loggers might need to change to more easily react to LLMs and other tools. For example, @richlander has a prototype logger that emits markdown - I'm pretty sure that logger doesn't understand the relationship between 'outer' builds and 'inner' like TL does. If it built on this base and then it could more easily get that understanding for free.
I kind of think of this interface as a step towards a more user-friendly Logger programming model - for loggers that care about rendering the 'intent' of the build and reshaping/papering over MSBuild specifics in favor of a more user-centric model this base can hide a lot of the guts.
This is opened as a new PR because rebasing the previous iteration closed the PR and I was unable to open it.