Skip to content

Add cooperative slow-test diagnostics for browser#10128

Draft
Evangelink wants to merge 2 commits into
mainfrom
dev/amauryleve/browser-hang-diagnostics
Draft

Add cooperative slow-test diagnostics for browser#10128
Evangelink wants to merge 2 commits into
mainfrom
dev/amauryleve/browser-hang-diagnostics

Conversation

@Evangelink

@Evangelink Evangelink commented Jul 22, 2026

Copy link
Copy Markdown
Member

Browser and WASI test runs cannot use the HangDump extension because it depends on process APIs and named pipes. This adds a lightweight diagnostic path that identifies active tests which remain running without relying on unsupported browser threading or process facilities.

  • Track active tests by UID in the shared simplified Browser/WASI output device.
  • Emit durable [slow] still running after ... lines using the existing MTP_PROGRESS_SLOW_TEST_SECONDS setting (0 disables reporting).
  • Share one exponential threshold/backoff state across Browser/WASI diagnostics, terminal heartbeat output, and the Azure DevOps/GitHub Actions slow-test reporter base.
  • Skip obsolete catch-up thresholds after delayed polls while reporting the actual elapsed duration.
  • Keep test starts silent so immediate per-test progress remains a separate concern.
  • Reuse the localized slow-test message without adding public API.
  • Document that cooperative reporting diagnoses asynchronous hangs, but cannot run while a synchronous test blocks the sole WebAssembly thread.

Validation includes focused platform and extension unit tests, warning-free multi-target builds for the platform and both linked-source CI extensions, and publishing/running BrowserPlayground under Node. MSBuild validation was captured with binary logs.

Related to #2196, specifically #2196 (comment).

Track active tests internally in the simplified browser and WASI output path, and report long-running tests with exponential backoff without relying on threads, processes, or named pipes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2eba6c9f-f63f-4726-a0ce-f90ed2404431
Copilot AI review requested due to automatic review settings July 22, 2026 07:45

Copilot AI left a comment

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.

Pull request overview

Adds cooperative slow-test diagnostics for browser/WASI environments.

Changes:

  • Tracks active tests and reports slow tests with exponential backoff.
  • Shares terminal progress configuration and localized messaging.
  • Adds focused tests, API tracking, and browser documentation.
Show a summary per file
File Description
SimplifiedConsoleOutputDeviceTests.cs Tests tracking and reporter lifecycle.
ActiveTestTrackerTests.cs Tests thresholds, backoff, and UID handling.
TerminalOutputDevice.Initialization.cs Uses shared progress configuration.
TerminalOutputDevice.cs Removes relocated constants.
SimplifiedConsoleOutputDeviceBase.cs Integrates tracking and cooperative reporting.
ProgressReportingConfiguration.cs Centralizes environment-based thresholds.
ActiveTestTracker.cs Implements active-test tracking and backoff.
InternalAPI.Unshipped.txt Tracks new internal APIs.
BrowserPlayground/README.md Documents slow-test diagnostics.

Review details

  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment on lines +402 to +405
if (nodeStateProp is InProgressTestNodeStateProperty)
{
_activeTestTracker.Start(testNodeStateChanged.TestNode.Uid, testNodeStateChanged.TestNode.DisplayName);
}
Comment on lines +134 to +136
Set `MTP_PROGRESS_SLOW_TEST_SECONDS` to a non-negative integer to change the first
reporting threshold; `0` disables these diagnostics. Test starts are tracked silently,
so this does not duplicate normal per-test progress output.
@Evangelink
Evangelink marked this pull request as draft July 22, 2026 07:52
Comment on lines +509 to +511
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10128

GradeTestNotes
F (0–59) new SimplifiedConsoleOutputDeviceTests.
SessionLifecycle_
CancelsAndAwaitsCooperativeReporter
No assertions — test exercises lifecycle methods but never verifies cancellation or awaiting actually occurred; consider asserting on a side-effect or mock invocation.
A (90–100) new ActiveTestTrackerTests.
Complete_
TracksSameNameTestsIndependentlyByUid
No issues found.
A (90–100) new ActiveTestTrackerTests.
GetDueDiagnostics_
UsesExponentialBackoff
Clear AAA; verifies uid, display name, and elapsed time across two backoff cycles.
A (90–100) new ActiveTestTrackerTests.
GetDueDiagnostics_
WhenPollIsDelayed_
ReportsOnceAndSkipsCatchUpThresholds
No issues found.
A (90–100) new ActiveTestTrackerTests.
Start_
WhenThresholdIsZero_
DoesNotTrackTests
No issues found.
A (90–100) new ActiveTestTrackerTests.
Start_
WhenUidIsAlreadyActive_
DoesNotResetElapsedTimeOrBackoff
No issues found.
A (90–100) new SimplifiedConsoleOutputDeviceTests.
ConsumeAsync_
CompletedTest_
IsRemovedFromSlowTestTracking
No issues found.
A (90–100) new SimplifiedConsoleOutputDeviceTests.
ConsumeAsync_
InProgressTest_
ReportsOnlyAfterSlowThreshold
Clear AAA with FakeClock; verifies count, slow tag, display name, and elapsed format.
A (90–100) new SimplifiedConsoleOutputDeviceTests.
ConsumeAsync_
SameNameTests_
AreTrackedIndependentlyByUid
Verifies count after uid-1 completes; consider also asserting which uid's name appears to fully confirm isolation.
A (90–100) new SimplifiedConsoleOutputDeviceTests.
CooperativeReporter_
WhenTestIsDue_
EmitsSlowDiagnostic
No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · 60.3 AIC · ⌖ 4.46 AIC · ⊞ 8.9K · [◷]( · )

@github-actions github-actions Bot left a comment

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.

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Summary

Well-structured PR that adds cooperative slow-test diagnostics to the simplified console output device, enabling browser-wasm scenarios where timers/threads are unavailable. The design is sound: async polling with exponential backoff, clean separation of tracker logic, and good test coverage.

Findings

Severity Finding
⚠️ Medium Potential race in OnTestSessionFinishingAsync — fields _slowTestReporterCancellationTokenSource and _slowTestReporterTask are read/nulled without atomicity guarantees. Use Interlocked.Exchange.
💡 Low GetDueDiagnostics() allocates on every poll even when no tests are due (empty list → array). Consider early-return with cached empty array or avoid the list allocation.
💡 Low The reporter won't restart if a second test session starts after the first finishes (flag not reset). Probably fine for current usage but worth a comment.

Verdict

The race condition in session teardown is the only item worth addressing before merge — the rest are suggestions. Overall this is clean, well-tested code with good documentation.

@@ -224,6 +275,11 @@ public Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext)
if (_firstCallTo_OnSessionStartingAsync)

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.

The CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) captures the session-starting cancellation token, but if OnTestSessionStartingAsync is called more than once (the guard only gates the first call), the second invocation is a no-op. However, if a second session does start after the first finishes (edge case), the fields were nulled in OnTestSessionFinishingAsync but the _firstCallTo_OnSessionStartingAsync flag is never reset, so the reporter won't restart. This is likely fine for single-session hosts, but worth a comment for maintainability.

{
return [];
}

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.

Minor: GetDueDiagnostics() allocates a List<SlowTestDiagnostic> and then converts it to an array on every poll (every 1 second by default), even when no tests are due. Consider using a pooled list or building the array directly when the count is known to be small, to reduce GC pressure in the hot path — especially relevant for browser-wasm where GC pauses are more visible.

{
return long.MaxValue;
}

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.

Good: the overflow guard (nextThresholdTicks > long.MaxValue / 2) prevents infinite loops for extremely long-running tests. Clean implementation.

@@ -207,9 +234,33 @@ public async Task DisplayAfterSessionEndRunAsync(CancellationToken cancellationT

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.

Race condition: _slowTestReporterCancellationTokenSource and _slowTestReporterTask are read without synchronization. If OnTestSessionFinishingAsync is called concurrently (e.g. abort path races with normal completion), one caller could read the CTS while another nulls it. Consider using Interlocked.Exchange to atomically swap both fields to null, ensuring exactly one caller cancels and awaits.

Extract exponential threshold state for reuse by browser and WASI diagnostics, terminal heartbeat output, and CI slow-test reporters.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2eba6c9f-f63f-4726-a0ce-f90ed2404431
Copilot AI review requested due to automatic review settings July 22, 2026 10:49

Copilot AI left a comment

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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 3
  • Review effort level: Medium

Comment on lines +151 to +155
TimeSpan elapsed = detail.Stopwatch.Elapsed;
SlowTestThresholdState thresholdState = _slowTestThresholdStates.TryGetValue(detail.Id, out SlowTestThresholdState? stored)
? stored
: new(_slowTestThreshold);
if (!thresholdState.IsDue(elapsed))
/// </remarks>
internal async Task ReportSlowTestsOnceAsync(CancellationToken cancellationToken)
{
SlowTestDiagnostic[] diagnostics = _activeTestTracker.GetDueDiagnostics();
Comment on lines +61 to +73
var diagnostics = new List<SlowTestDiagnostic>();
foreach ((TestNodeUid uid, ActiveTest activeTest) in _activeTests.OrderBy(static pair => pair.Key.Value, StringComparer.Ordinal))
{
TimeSpan elapsed = activeTest.Stopwatch.Elapsed;
if (!activeTest.SlowTestThreshold.IsDue(elapsed))
{
continue;
}

diagnostics.Add(new(uid, activeTest.DisplayName, elapsed));
}

return [.. diagnostics];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants