Skip to content

Log real elapsed time (and hangs) in SlowOperation telemetry#41092

Open
yeelam-gordon wants to merge 1 commit into
microsoft:masterfrom
yeelam-gordon:yeelam-gordon/slowoperation-elapsed
Open

Log real elapsed time (and hangs) in SlowOperation telemetry#41092
yeelam-gordon wants to merge 1 commit into
microsoft:masterfrom
yeelam-gordon:yeelam-gordon/slowoperation-elapsed

Conversation

@yeelam-gordon

Copy link
Copy Markdown
Contributor

Why

Today''s SlowOperation telemetry (from #40269) fires once at 10 s and stops, with no duration — so every slow phase looks identical. We can''t tell an 11 s blip from a 5-minute stall; a true hang (operation never returns, destructor never runs) is invisible; and the backend just accumulates a pile of "crossed 10 s" events that carry no insight and nothing to act on.

With the real elapsed time we can bucket durations on a dashboard (e.g. 10–30 s / 30–60 s / 1–5 min / hung) and actually see where startup time goes and which phase is regressing — instead of only knowing "something was slow." #40269''s own review flagged this as the follow-up: "we know it was slow, but not how slow… logging elapsed at scope exit would be most useful."

What (only SlowOperationWatcher; no call sites touched)

At most one event, now carrying the real duration:

  • < 10 s → nothing (unchanged fast path).
  • ≥ 10 s → one timedOut=false event with the true elapsedMs.
  • still running at MaxDuration (15 min) → one timedOut=true event, then stop.

timedOut is a finished-vs-hung flag, deliberately not success-vs-failure: the watcher is a pure duration timer with no visibility into the operation''s result (a scope that exits by throwing is still timedOut=false), and WSL failures often don''t throw, so any inferred success/failure would be unreliable. elapsedMs + timedOut are added fields (backward compatible); new ctor params are defaulted (all 12 callers unchanged); only WSL_LOG_TELEMETRY is emitted; construction is best-effort and fully noexcept, and the timer is drained before teardown → no UAF, and a telemetry failure can never crash or slow the watched operation.

Tests (new SlowOperationWatcherUnitTests.cpp, TAEF — 6/6 passing)

  • FastPathEmitsNothing / DefaultConstructionFastPathEmitsNothing — under threshold stays silent.
  • SlowCompletionEmitsOneRecord — one timedOut=false with real elapsed.
  • ResetAfterThresholdEmitsOnceReset() + destructor emit exactly once.
  • ResetBeforeThresholdEmitsNothing — early disarm stays silent, even if the scope runs long after Reset().
  • HangEmitsOnceAtMaxThenStops — one timedOut=true at the cap, no second event on later exit.

Copilot AI review requested due to automatic review settings July 16, 2026 14: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.

Pull request overview

This PR enhances SlowOperationWatcher telemetry so slow-operation events carry the actual elapsed time and can also surface true hangs via a max-duration backstop, enabling meaningful duration bucketing and distinguishing “slow but finished” from “hung”.

Changes:

  • Updated SlowOperationWatcher to emit at most one telemetry record with elapsedMs and timedOut, emitting on completion (destructor/Reset()) or at MaxDuration for hangs.
  • Added a best-effort max-duration threadpool timer backstop and an injectable sink to support deterministic unit testing.
  • Added new TAEF unit tests and wired them into the Windows test CMake target.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/windows/common/SlowOperationWatcher.h Extends the watcher contract/API to include max-duration hang detection, event payload struct, and sink injection.
src/windows/common/SlowOperationWatcher.cpp Implements single-emission logic (m_reported), real elapsed measurement, hang backstop timer, and updated telemetry fields.
test/windows/SlowOperationWatcherUnitTests.cpp Adds unit tests validating fast/slow/reset/hang behaviors via an injected recording sink.
test/windows/CMakeLists.txt Adds the new unit test source file to the Windows test build.

Comment thread test/windows/SlowOperationWatcherUnitTests.cpp

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread src/windows/common/SlowOperationWatcher.cpp

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread src/windows/common/SlowOperationWatcher.h
Follow-up to microsoft#40269, which introduced SlowOperationWatcher. As noted in that PR's
review, when the threshold timer fires we learn only that an operation was slow,
not *how* slow -- it could have finished at 10.1 s or still be stuck at 60 s -- and
"logging the elapsed time at scope exit could be the most useful" follow-up. This
adds that, while keeping the watcher a passive, telemetry-only observer that emits
**at most one** event.

Behavior (exactly one of):
- Fast path: the operation finishes under SlowThreshold (10 s default) -- the common
  case. Nothing is emitted.
- Slow completion: it finishes but took >= SlowThreshold. On scope exit (destructor /
  Reset()) one `timedOut=false` SlowOperation event is emitted with the real
  `elapsedMs` since construction -- the authoritative total duration.
- Hang backstop: it is still running at MaxDuration (15 min default). A single-shot
  timer fires once and emits one `timedOut=true` event (elapsedMs ~= MaxDuration),
  then stops -- so an operation that never returns (e.g. an HCS wait that blocks
  INFINITE, where the destructor never runs) is still reported exactly once instead
  of being invisible. It never repeats.

So SlowThreshold is the "slow enough to report at completion" filter and MaxDuration
is the "give up waiting and report the hang, once" deadline. m_reported (atomic)
guarantees at most one event across the timer callback, Reset(), and the destructor.

The event schema gains `elapsedMs` (Int64) and `completed` (Boolean); the existing
`name`, `thresholdMs`, `file`, `function`, `line` fields are unchanged (additive,
backward compatible).

Compatibility / safety:
- The two new constructor parameters (MaxDuration, OnSlow) are defaulted, so every
  existing call site compiles and behaves identically; the fast path emits nothing.
- Only WSL_LOG_TELEMETRY is emitted (which also lands in ETL) -- no second log.
- The threadpool callback dereferences the watcher, but Finish() resets the timer
  (cancel + drain in-flight callback) before any member is destroyed, so there is no
  use-after-free. The emission path is fully noexcept + CATCH_LOG guarded, so a
  telemetry failure can never crash or alter the watched operation.
- No call sites are added or moved; the change is confined to the watcher class.

Tests: adds test/windows/SlowOperationWatcherUnitTests.cpp (TAEF) covering the fast
path, default-constructed fast path, slow completion (one completed=true with real
elapsed), Reset() at-most-once, and the hang backstop (one completed=false at max,
no second event on scope exit) -- via an injected recording sink.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f7fab85-2f28-4344-9454-b76e8d2cc4d4
Copilot AI review requested due to automatic review settings July 16, 2026 15:10
@yeelam-gordon
yeelam-gordon force-pushed the yeelam-gordon/slowoperation-elapsed branch from b576d01 to 8778ff4 Compare July 16, 2026 15:10

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines +165 to +176
constexpr auto threshold = 50ms;
{
SlowOperationWatcher watcher{"ResetFast", threshold, c_noHang, &RecordSink};

// The watched operation finished quickly (under threshold) -> Reset() here.
watcher.Reset();
VERIFY_ARE_EQUAL(g_recorder.Snapshot().size(), static_cast<size_t>(0));

// Simulate slow, unrelated work AFTER Reset() that pushes total elapsed well
// past the threshold. The destructor at scope exit must still emit nothing.
std::this_thread::sleep_for(150ms);
}
@yeelam-gordon
yeelam-gordon marked this pull request as ready for review July 19, 2026 23:58
@yeelam-gordon
yeelam-gordon requested a review from a team as a code owner July 19, 2026 23:58
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