Skip to content

perf(bigtable): move recordTimeSeries + sampleActiveUptimes to a 1-min loop - #20283

Open
sushanb wants to merge 1 commit into
googleapis:mainfrom
sushanb:fix/bigtable-uptime-sample-cadence
Open

sushanb wants to merge 1 commit into
googleapis:mainfrom
sushanb:fix/bigtable-uptime-sample-cadence

Conversation

@sushanb

@sushanb sushanb commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Both `recordTimeSeries` and `sampleActiveUptimes` were running every `Tick` (1-second cadence), but they're periodic-snapshot metrics — nothing changes meaningfully at 1s granularity. `sampleActiveUptimes` calls `sessionUptime.Record` on every Ready session per Tick, so at 100 sessions this was ~6,000 OTel histogram writes / min plus the accompanying `toOtelMetricAttrs` allocations (visible in heap profiles as multi-MB residence).

Matches java-bigtable's `session.uptime` cadence at `MetricsImpl.java:193`: `scheduleAtFixedRate(this::recordAsyncSessionMetrics, 1, 1, TimeUnit.MINUTES)`.

Changes

  • Drop the two calls from `Tick`'s body (`session_pool_scaling.go`).
  • Add `slowMetricsInterval = 1 * time.Minute` and `startSlowMetricsLoop`, mirroring the existing `startSweepStuckSessionsLoop` / `startAfePruneLoop` patterns — one dedicated goroutine per periodic concern.
  • Wire into `Start()` alongside the other periodic loops.

`Tick` keeps its 1-s cadence for sizing decisions (still needed for reactive scale-up on burst arrivals — Java handles that via event-driven hooks, but Go's current shape uses Tick).

Impact estimate

For a pool with N sessions:

Metric Before After Reduction
`sessionUptime.Record` calls / min N × 60 N 60×
`recordTimeSeries` calls / min 60 1 60×
`toOtelMetricAttrs` allocations / min proportional to above proportional to above 60×

At the 100-session sandbox pod: from ~6,000 histogram writes/min → 100. Heap-profile residency for `toOtelMetricAttrs` should drop correspondingly.

Test plan

  • `go build ./...` clean.
  • `go vet ./internal/transport/` clean.
  • `go test -race -count=1 -short -timeout=120s ./internal/transport/` passes.
  • Verified `Tick` no longer calls the two functions; new loop shape mirrors existing loops.

Verification (post-merge)

  • Heap profile: `toOtelMetricAttrs` cumulative should shrink.
  • OTel export volume for `session.uptime` histogram drops ~60×.
  • Sessionz time-series graph updates once per minute instead of once per second — visually the same for aggregate views; per-second granularity was already lost in downstream sampling.

…loop

Both were running every Tick (1-second cadence), but they're
periodic-snapshot metrics — nothing changes meaningfully at 1 s
granularity. sampleActiveUptimes calls sessionUptime.Record on
every Ready session per Tick, so at 100 sessions this was ~6000
OTel histogram writes / min + the accompanying toOtelMetricAttrs
allocations (visible in heap profiles as multi-MB residence).

Matches java-bigtable's session.uptime cadence at MetricsImpl.java:
193 — scheduleAtFixedRate(this::recordAsyncSessionMetrics, 1, 1,
TimeUnit.MINUTES).

Changes:
- Drop the two calls from Tick's body (session_pool_scaling.go).
- Add slowMetricsInterval = 1 minute and startSlowMetricsLoop,
  mirroring the existing startSweepStuckSessionsLoop / start
  AfePruneLoop patterns (dedicated goroutine per concern).
- Wire into Start() alongside the other periodic loops.

Tick keeps its 1-s cadence for sizing decisions (still needed for
reactive scale-up on burst arrivals — Java handles that via
event-driven hooks, but Go's current shape uses Tick).
@sushanb
sushanb requested review from a team as code owners July 31, 2026 17:56
@product-auto-label product-auto-label Bot added api: bigtable Issues related to the Bigtable API. samples Issues that are directly related to samples. labels Jul 31, 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 decouples the metrics collection (recordTimeSeries and sampleActiveUptimes) from the 1-second Tick loop, moving them to a separate 1-minute interval loop (startSlowMetricsLoop) to reduce OTel histogram write overhead. The reviewer noted that this introduces a 1-minute delay for the initial metrics collection, which could lead to missing metrics in short-lived processes, and suggested executing an initial collection immediately upon starting the loop.

Comment thread bigtable/internal/transport/session_pool_lifecycle.go
sushanb added a commit that referenced this pull request Jul 31, 2026
…0285)

> Stacked on **#20283** (\`fix/bigtable-uptime-sample-cadence\`). Do not
merge before that lands.

## Summary

After #20283 moved \`recordTimeSeries\` and \`sampleActiveUptimes\` to a
dedicated 1-min loop, \`Tick\`'s body is pure sizing (\`sizer.Decide\` +
\`createSession\` fan-out). Every meaningful pool-state transition
already fires \`spawnTickOnce\` so a background timer is redundant.

## Event coverage audit

| Trigger | Site | Fires |
|---|---|---|
| Session became Ready | \`onActive\` | \`signalFree\` (wakes waiters) —
no Tick needed since a Ready-count increment doesn't want more sessions
|
| Session leaving Ready | \`onClosing\`
(\`session_pool_lifecycle.go:326\`) | \`spawnTickOnce\` — kicks a
replacement decision |
| Waiter park on empty pool | \`CheckoutSession\`
(\`session_pool.go:256\`) | \`spawnTickOnce\` — kicks a cold-start /
burst-scale decision |
| Server-driven config change | \`UpdateConfig\` (**new** in this PR) |
\`spawnTickOnce\` — covers the one path where none of the above events
fire (e.g., \`MinSessionCount\` bumped on an idle pool) |
| Pool cold start | \`Start()\` (\`session_pool_lifecycle.go\`) |
Pre-start \`spawnTickOnce\` seeds MinSessions |

\`spawnTickOnce\` is CAS-guarded via \`tickPending\`, so a burst of
listener fires coalesces to one Tick body.

## Java parity

Matches java-bigtable's fully event-driven \`poolSizer\`: no periodic
timer for sizing. Sizing decisions ride on the events that would trigger
them anyway.

## Impact

- **Idle pool steady-state:** was ~1 Tick / sec (\`sizer.Decide\` +
\`p.mu\` bracket + \`sl\` walk) = ~200-500 ns of CPU + lock contention
per pool per second. **Now zero** between events.
- **Under load:** unchanged — same event-driven scale-up paths fire.
- **Under bursty saturation:** unchanged — \`CheckoutSession\`'s
empty-pool kick already covered burst reaction, not the periodic Tick.

## Changes

- Delete \`startTickLoop\` + \`tickInterval\` const
(\`session_pool_lifecycle.go\`).
- Drop \`p.startTickLoop(ctx)\` from \`Start()\`; keep the pre-start
\`spawnTickOnce\` that seeds MinSessions.
- Add \`p.spawnTickOnce(p.poolCtx)\` at the end of \`UpdateConfig\`
(\`session_pool.go\`).

Net: +9 / -25 lines.

## Test plan

- [x] \`go build ./...\` clean.
- [x] \`go vet ./internal/transport/\` clean.
- [x] \`go test -race -count=5 -short -timeout=180s -run
'TestSessionPool|TestCheckoutSession|TestPool_' ./internal/transport/\`
— 5× stress pass.
- [x] \`go test -race -count=1 -short ./internal/session/\` — pass.

## What this does NOT change

- \`Tick\` itself (\`SessionPoolImpl.Tick\`) is unchanged — it just no
longer runs on a timer. \`spawnTickOnce\` / \`tickOnce\` still wrap it
with debounce + panic recovery.
- \`sweepStuckSessions\` (its own 30 s loop), AFE prune (its own loop),
slow-metrics (its own 1-min loop from #20283) — all unchanged.
- \`Close\` teardown — \`spawns\` WaitGroup semantics unchanged since
\`spawnTickOnce\` still bumps \`spawns.Add\` before the goroutine.
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. samples Issues that are directly related to samples.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant