From 58828852612e13d897c882a3e357ae488c887759 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 12 Mar 2026 18:55:40 +0100 Subject: [PATCH 1/3] Fix ScheduleTimeRecord.AccumulatedTime throwing when timer is running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScheduleTimeRecord.AccumulatedTime throws InternalErrorException with 'Can't get the accumulated time while the timer is still running' during Scheduler.WriteDetailedSummary(). This exception kills the BuildManager work queue, preventing any further build results from being processed. EndBuild() hangs indefinitely, causing VS to freeze for hours. The fix returns the best-effort elapsed time (accumulated + current elapsed) when the timer is still running, instead of throwing. This is diagnostic summary data — throwing has no correctness benefit but causes a catastrophic hang. 11 hits in 30 days confirmed via telemetry (StackHash: 2C721D65...). All occurrences during solution close with running timers. --- .../Components/Scheduler/ScheduleTimeRecord.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs b/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs index 8a79db3505b..54f9d59542d 100644 --- a/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs +++ b/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs @@ -32,12 +32,22 @@ public ScheduleTimeRecord() /// /// Retrieve the accumulated time. + /// If the timer is still running, returns the accumulated time so far + /// (elapsed time since the timer started plus any previously accumulated time) + /// instead of throwing. This prevents a crash in diagnostic summary logging + /// from killing the BuildManager work queue and hanging VS indefinitely. + /// See: https://github.com/dotnet/msbuild/issues/XXXXX /// public TimeSpan AccumulatedTime { get { - ErrorUtilities.VerifyThrow(_startTimeForCurrentState == DateTime.MinValue, "Can't get the accumulated time while the timer is still running."); + if (_startTimeForCurrentState != DateTime.MinValue) + { + // Timer is still running — return best-effort elapsed time. + return _accumulatedTime + (DateTime.UtcNow - _startTimeForCurrentState); + } + return _accumulatedTime; } } From 161868d0702e342803ea5bf38fb3cb97c7dc6f87 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 12 Mar 2026 19:09:16 +0100 Subject: [PATCH 2/3] Address review comments: remove placeholder URL, add unit tests - Remove placeholder issues/XXXXX URL from XML doc comment - Add ScheduleTimeRecord_AccumulatedTime_DoesNotThrowWhenTimerIsRunning test - Add ScheduleTimeRecord_AccumulatedTime_IncludesPreviousAccumulation test --- .../BackEnd/Scheduler_Tests.cs | 48 +++++++++++++++++++ .../Scheduler/ScheduleTimeRecord.cs | 4 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs b/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs index d7997e94888..3d8300e1801 100644 --- a/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs +++ b/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs @@ -968,5 +968,53 @@ private void ReportDefaultParentRequestIsFinished() var buildResult = new BuildResult(_defaultParentRequest); _scheduler.ReportResult(_defaultParentRequest.NodeRequestId, buildResult); } + + [Fact] + public void ScheduleTimeRecord_AccumulatedTime_DoesNotThrowWhenTimerIsRunning() + { + var record = new ScheduleTimeRecord(); + DateTime startTime = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + // Before starting: accumulated time should be zero. + record.AccumulatedTime.ShouldBe(TimeSpan.Zero); + + // Start the timer. + record.StartState(startTime); + + // While running: should NOT throw — should return a positive elapsed time. + TimeSpan elapsed = record.AccumulatedTime; + elapsed.ShouldBeGreaterThan(TimeSpan.Zero); + + // Stop the timer. + DateTime endTime = startTime.AddMilliseconds(500); + record.EndState(endTime); + + // After stopping: should return the accumulated time. + record.AccumulatedTime.ShouldBe(TimeSpan.FromMilliseconds(500)); + } + + [Fact] + public void ScheduleTimeRecord_AccumulatedTime_IncludesPreviousAccumulation() + { + var record = new ScheduleTimeRecord(); + DateTime t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + // First interval: 200ms. + record.StartState(t0); + record.EndState(t0.AddMilliseconds(200)); + record.AccumulatedTime.ShouldBe(TimeSpan.FromMilliseconds(200)); + + // Start a second interval. + record.StartState(t0.AddMilliseconds(300)); + + // While second interval is running: should include the 200ms from + // the first interval plus the elapsed time of the current interval. + TimeSpan elapsed = record.AccumulatedTime; + elapsed.ShouldBeGreaterThan(TimeSpan.FromMilliseconds(200)); + + // Stop second interval after 100ms. + record.EndState(t0.AddMilliseconds(400)); + record.AccumulatedTime.ShouldBe(TimeSpan.FromMilliseconds(300)); + } } } diff --git a/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs b/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs index 54f9d59542d..3ff10fb52a4 100644 --- a/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs +++ b/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs @@ -34,9 +34,7 @@ public ScheduleTimeRecord() /// Retrieve the accumulated time. /// If the timer is still running, returns the accumulated time so far /// (elapsed time since the timer started plus any previously accumulated time) - /// instead of throwing. This prevents a crash in diagnostic summary logging - /// from killing the BuildManager work queue and hanging VS indefinitely. - /// See: https://github.com/dotnet/msbuild/issues/XXXXX + /// instead of throwing. /// public TimeSpan AccumulatedTime { From a2da4d8b192edeb91e59cbe1beb98e5900892059 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:50:56 +0100 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Rainer Sigwald --- src/Build.UnitTests/BackEnd/Scheduler_Tests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs b/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs index 3d8300e1801..dab27885584 100644 --- a/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs +++ b/src/Build.UnitTests/BackEnd/Scheduler_Tests.cs @@ -982,8 +982,7 @@ public void ScheduleTimeRecord_AccumulatedTime_DoesNotThrowWhenTimerIsRunning() record.StartState(startTime); // While running: should NOT throw — should return a positive elapsed time. - TimeSpan elapsed = record.AccumulatedTime; - elapsed.ShouldBeGreaterThan(TimeSpan.Zero); + record.AccumulatedTime.ShouldBeGreaterThan(TimeSpan.Zero); // Stop the timer. DateTime endTime = startTime.AddMilliseconds(500); @@ -1009,8 +1008,7 @@ public void ScheduleTimeRecord_AccumulatedTime_IncludesPreviousAccumulation() // While second interval is running: should include the 200ms from // the first interval plus the elapsed time of the current interval. - TimeSpan elapsed = record.AccumulatedTime; - elapsed.ShouldBeGreaterThan(TimeSpan.FromMilliseconds(200)); + record.AccumulatedTime.ShouldBeGreaterThan(TimeSpan.FromMilliseconds(200)); // Stop second interval after 100ms. record.EndState(t0.AddMilliseconds(400));