Skip to content

fix(bigtable): measure real scheduling delay in pacemaker_delays - #20445

Merged
mutianf merged 2 commits into
googleapis:mainfrom
mutianf:bigtable-pacemaker-scheduling-delay
Sep 1, 2026
Merged

mutianf merged 2 commits into
googleapis:mainfrom
mutianf:bigtable-pacemaker-scheduling-delay

Conversation

@mutianf

@mutianf mutianf commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

The bug

Pacemaker is meant to report how long its goroutine waits to be scheduled, as a proxy for CPU contention. It computed that as the gap between consecutive tick timestamps, minus the interval:

actualInterval := t.Sub(lastTick)
delay := actualInterval - interval

t is not the delivery time. time.sendTime backdates every tick to the time it was due, and the runtime's own comment says why:

// delta is how long ago the channel send was supposed to happen.
// The current time can be arbitrarily far into the future, because the runtime
// can delay a sendTime call until a goroutine tries to receive from
// the channel. Subtract delta to go back to the old time that we used to send.
case c.(chan Time) <- Now().Add(Duration(-delta)):

Consecutive due times are exactly one interval apart by construction, so actualInterval - interval is zero unless the runtime drops a tick outright. The metric could only see starvation lasting longer than a full 100ms interval, and only in 100ms steps — meaning its eight bucket boundaries below 100ms (100us through 50ms) recorded nothing but timer jitter.

Measured with 32 CPU-bound goroutines on GOMAXPROCS=1, comparing what the code recorded against the goroutine's true wait:

tick   as-written    receive-vs-due
0      0s            16ms
1      0s            17ms
2      439ns         18ms
3      0s            19ms
4      0s            20ms
...
as-written recorded exactly 0 on 15/30 ticks

Up to 20ms of real scheduling delay, reported as 573ns at worst.

The fix

Compare the due time against the time we actually read the tick. Since the channel value is the due time, that difference is the scheduling delay directly — no dependence on dropped ticks, no quantization by the interval. Under the same 32-goroutine workload the metric now reports a mean of 8.7ms.

The computation moves into a recordTick method so the semantics can be tested without timing dependence.

Note for reviewers

This changes what an existing exported metric reports: pacemaker_delays will go from near-zero to real values on any contended client. That is the point of the change, but if anything alerts on this metric today, the thresholds were calibrated against a signal that was always ~0 and will need revisiting.

Testing

  • TestPacemakerRecordTick — table test pinning the semantics, including the case the old code could not represent (starvation shorter than one interval, which drops no ticks).
  • TestPacemakerRecordTickNeverNegative — a due time in the future records 0, not a negative sample.
  • Existing TestPacemakerExporting still passes; full go test -race ./internal/transport/ green.

The pacemaker computed its delay as the gap between consecutive tick
timestamps, minus the tick interval. Those timestamps are not delivery
times: time.sendTime backdates every tick to the time it was due
(Now().Add(-delta)), precisely so that a late delivery still reports the
old time. Consecutive due times are therefore exactly one interval apart
by construction, and the subtraction yields zero unless the runtime
drops a tick outright.

So the metric could only observe starvation lasting longer than a full
100ms interval, and only in 100ms steps -- its eight buckets below 100ms
held nothing but timer jitter. Measured with 32 CPU-bound goroutines on
GOMAXPROCS=1, the pacemaker goroutine waited up to 20ms to be scheduled
while the metric recorded exactly 0 on half the ticks and 573ns at worst.

Compare the due time against the time we actually read the tick instead.
That is the scheduling delay directly, with no dependence on dropped
ticks and no quantization by the interval. Under the same workload the
metric now reports a mean of 8.7ms.

Change-Id: Iaeaa90c6c60fcec516b9a3f13e2f2313c5b04f8a
@mutianf
mutianf requested review from a team as code owners September 1, 2026 14:25
@product-auto-label product-auto-label Bot added the api: bigtable Issues related to the Bigtable API. label Sep 1, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the Pacemaker scheduling delay monitor to measure latency by comparing the actual tick handling time against its scheduled due time, rather than comparing consecutive tick timestamps. This allows the pacemaker to detect starvation shorter than a full interval. Corresponding unit tests were added to verify the accuracy of the recorded delays and ensure negative delays are never recorded. The feedback suggests a minor improvement in recordTick to use Go's idiomatic time.Microsecond constant for duration conversion instead of hardcoded division.

if delay < 0 {
delay = 0
}
p.histogram.Record(ctx, float64(delay.Nanoseconds())/1e3, p.attrs)

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.

medium

Instead of hardcoding 1e3 and calling Nanoseconds(), it is more idiomatic and readable in Go to divide the time.Duration by time.Microsecond to convert it to a fractional microsecond value. This avoids magic numbers and leverages the standard library's duration constants.

Suggested change
p.histogram.Record(ctx, float64(delay.Nanoseconds())/1e3, p.attrs)
p.histogram.Record(ctx, float64(delay)/float64(time.Microsecond), p.attrs)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in ca2fe36. Confirmed numerically identical to the previous form (bit-for-bit across 5M random durations and at the int64 extremes), so this is a pure readability change — and it matches session_tracer.go:315,329 in this same package, which already records durations into histograms as float64(x)/float64(time.Millisecond).

…ersion

Matches session_tracer.go, which records durations into histograms the
same way. Numerically identical to the previous form.

Change-Id: I86c9c8ed2ab1bf65128055640b01bde04896400d
@mutianf
mutianf merged commit e1304d6 into googleapis:main Sep 1, 2026
19 checks passed
@mutianf
mutianf deleted the bigtable-pacemaker-scheduling-delay branch September 1, 2026 16:33
haphungw pushed a commit that referenced this pull request Sep 2, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.54.0](bigtable/v1.53.0...bigtable/v1.54.0)
(2026-09-02)


### Features

* **bigtable:** Serve grpc.health.v1.Health from the accelerator daemon
([#20446](#20446))
([5fba38a](5fba38a))
* Update API sources and regenerate
([#20459](#20459))
([5426e32](5426e32))


### Bug Fixes

* **bigtable:** Measure real scheduling delay in pacemaker_delays
([#20445](#20445))
([e1304d6](e1304d6))
* **bigtable:** Scope session pool maintenance loops to the pool, not
the client
([#20444](#20444))
([7457e6e](7457e6e))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: bigtable Issues related to the Bigtable API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants