Repository navigation
fix(bigtable): measure real scheduling delay in pacemaker_delays - #20445
Conversation
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| p.histogram.Record(ctx, float64(delay.Nanoseconds())/1e3, p.attrs) | |
| p.histogram.Record(ctx, float64(delay)/float64(time.Microsecond), p.attrs) |
There was a problem hiding this comment.
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
🤖 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>
The bug
Pacemakeris 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:tis not the delivery time.time.sendTimebackdates every tick to the time it was due, and the runtime's own comment says why:Consecutive due times are exactly one interval apart by construction, so
actualInterval - intervalis 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: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
recordTickmethod so the semantics can be tested without timing dependence.Note for reviewers
This changes what an existing exported metric reports:
pacemaker_delayswill 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.TestPacemakerExportingstill passes; fullgo test -race ./internal/transport/green.