Skip to content

Report GC time and full GC count for every isolate lock - #8099

Merged
danlapid merged 2 commits into
mainfrom
dlapid/lock-gc-stats
Nov 2, 2026
Merged

danlapid merged 2 commits into
mainfrom
dlapid/lock-gc-stats

Conversation

@danlapid

@danlapid danlapid commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

LockTiming used to time GC itself, through GC prologue and epilogue hooks, and only when the request was traced in Jaeger. Those hooks bracket only the atomic pause, so incremental marking steps run on the main thread went uncounted.

The isolate lock now measures GC with V8's LongTaskStats: it resets the stats when the lock is taken and reads them before release. That covers young collections, full atomic pauses and incremental marking. The lock also counts the full (mark-compact) collections that finish while it is held. Both values go to the new LockTiming::reportGc(), which replaces gcPrologue() and gcEpilogue(), and it is called on every lock, not only traced ones.

Downstream, edgeworker uses this to publish per-invocation GC time and full GC count in its invocation logs. Nothing else implements LockTiming's GC hooks.

About LongTaskStats being experimental

v8::metrics::LongTaskStats is marked experimental in v8-metrics.h, but it's well established:

  • It was added in April 2021 (V8 commit 521ae93bf65, by the V8 GC team). Its API was last extended in December 2021, when v8_execute_us was added, and has only seen mechanical refactors since.
  • Chromium uses it in production. Blink calls Reset() at the start of every main-thread task (PerformanceMonitor::WillProcessTask) and Get() for the PerformanceAPI.LongTask UKM (RecordLongTaskUkm in core/timing/performance.cc). Blink computes Duration.V8.GC from the same three-field sum we use. The V8 GC leads own that UKM.
  • V8 tests it in cctest (LongTaskStatsFullAtomic, LongTaskStatsFullIncremental, ...).

If it is removed, the build breaks at the V8 upgrade. If it silently stops reporting, edgeworker's gc-logging test, which asserts that GC time is non-zero after forced GCs, catches it.

🤖 Generated with Claude Code

@danlapid
danlapid requested review from a team as code owners October 30, 2026 03:07

@clanky-the-clanker clanky-the-clanker Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded by the review of bc6c62f4.

This review, of 4104eca4

✅ Approved — No issues found: GC is measured over the whole time the lock is held, and the observer interface change is small and consistent with how it is called.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

📋 Code Quality: ✅ LGTM

No issues found. The LongTaskStats reset and read bracket the time the lock is held, the full-GC counter is kept per lock and incremented only for mark-compact epilogues, and WebAssembly instance cleanup in the GC prologue still works as before.


📝 Documentation: ✅ LGTM

No issues found. The new reportGc() doc comment and the updated comments where the callbacks are registered describe the current behaviour accurately.


Reviewed commit: 4104eca4

@codecov-commenter

codecov-commenter commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 68.31%. Comparing base (e48c3f9) to head (e777644).

Files with missing lines Patch % Lines
src/workerd/io/observer.h 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8099   +/-   ##
=======================================
  Coverage   68.31%   68.31%           
=======================================
  Files         726      726           
  Lines      189662   189670    +8     
  Branches    29672    29672           
=======================================
+ Hits       129567   129577   +10     
  Misses      42719    42719           
+ Partials    17376    17374    -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@uncleDecart uncleDecart left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, only thing I'd add is virtual destuctor to LockTiming class

@danlapid
danlapid force-pushed the dlapid/lock-gc-stats branch from 4104eca to bc6c62f Compare November 1, 2026 23:30
@danlapid

danlapid commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

@uncleDecart Added in bc6c62f: virtual ~LockTiming() noexcept(false) = default;, matching the other observer interfaces in observer.h. Also rebased onto main.

@clanky-the-clanker clanky-the-clanker Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded by the review of e7776447.

This review, of bc6c62f4

✅ Approved — No issues found: the only change since the last review is the requested virtual destructor on LockTiming, and the GC measurement logic is still correct.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

📋 Code Quality: ✅ LGTM

No issues found. virtual ~LockTiming() noexcept(false) = default; is correct for an interface owned through kj::Own<LockTiming>. The LongTaskStats reset happens after the lock is acquired, and the read happens before currentLock is cleared, so it brackets the whole time the lock is held. fullGcCount only goes up for kGCTypeMarkSweepCompact. LockRecord::reportGc does nothing when there is no LockTiming. Nothing in the tree still refers to the removed hooks.


📝 Documentation: ✅ LGTM

No issues found. The reportGc() doc comment and the updated comments at the places where the callbacks are registered match what the code does now.


Reviewed commit: bc6c62f4

danlapid and others added 2 commits November 1, 2026 21:29
LockTiming used to time GC itself, through GC prologue and epilogue
hooks, and only when the request was traced in Jaeger. Those hooks
bracket only the atomic pause, so incremental marking steps run on
the main thread went uncounted.

The isolate lock now measures GC with V8's LongTaskStats: it resets
the stats when the lock is taken and reads them before release,
which covers young collections, full atomic pauses and incremental
marking. It also counts the full (mark-compact) collections that
finish under the lock. Both are passed to the new
LockTiming::reportGc(), which replaces gcPrologue() and
gcEpilogue(), on every lock rather than only traced ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It is an interface that implementations subclass and that is owned
through kj::Own<LockTiming>, like the other observer interfaces in
this file, which already declare one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@danlapid
danlapid force-pushed the dlapid/lock-gc-stats branch from bc6c62f to e777644 Compare November 2, 2026 02:36

@clanky-the-clanker clanky-the-clanker Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved — No issues found: the only change since the last review is the requested virtual destructor on LockTiming, and the GC measurement logic is still correct.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

📋 Code Quality: ✅ LGTM

No issues found. virtual ~LockTiming() noexcept(false) = default; is correct for an interface owned through kj::Own<LockTiming>. The LongTaskStats reset happens after the lock is acquired, and the read happens before currentLock is cleared, so it brackets the whole time the lock is held. fullGcCount only goes up for kGCTypeMarkSweepCompact. LockRecord::reportGc does nothing when there is no LockTiming. Nothing in the tree still refers to the removed hooks.


📝 Documentation: ✅ LGTM

No issues found. The reportGc() doc comment and the updated comments at the places where the callbacks are registered match what the code does now.


Reviewed commit: e7776447 — same changes as bc6c62f4, so this is that commit's review (no new review was run; line numbers are as of bc6c62f4)

@danlapid
danlapid merged commit 881395c into main Nov 2, 2026
23 of 24 checks passed
@danlapid
danlapid deleted the dlapid/lock-gc-stats branch November 2, 2026 04:07
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.

5 participants