Add caller-side DuckDB CPU attribution metric#5642
Conversation
Adds a KurrentDB.DuckDB meter with a kurrentdb.duckdb.cpu.seconds counter (tags: activity=query|read|commit|checkpoint, source=caller) measuring CPU consumed by DuckDB operations executed on KurrentDB threads, so node CPU can be decomposed into KurrentDB vs DuckDB. ThreadCpuTime reads the calling thread's cumulative CPU time via clock_gettime(CLOCK_THREAD_CPUTIME_ID) on Linux/macOS and GetThreadTimes on Windows. Measurement scopes are ref structs so they cannot span an await, which would invalidate per-thread CPU deltas. Instrumented synchronous DuckDB sections: index commits, user index checkpoints, the index readers, and QueryEngine's prepare/execute section. CPU burned on DuckDB's internal worker threads and during streaming chunk fetches is not visible from the caller side; that share lands with the planned Quack external_threads work as source=workers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new caller-side DuckDB CPU attribution metric (kurrentdb.duckdb.cpu.seconds) to help decompose overall process CPU usage into “KurrentDB vs DuckDB” CPU consumption, and wires it through the standard metrics pipeline.
Changes:
- Added
ThreadCpuTime+DuckDBCpuMetricsto measure per-thread CPU deltas and emit a counter tagged withactivityandsource=caller. - Instrumented synchronous DuckDB sections in query execution and secondary index read/commit/checkpoint paths.
- Registered the new meter in
metricsconfig.jsonand added unit tests validating “busy” vs “idle” behavior.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/KurrentDB/metricsconfig.json | Enables exporting the new KurrentDB.DuckDB meter via the standard metrics endpoint. |
| src/KurrentDB.SecondaryIndexing/Query/QueryEngine.cs | Measures caller-side CPU for the synchronous query setup/prepare phase. |
| src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexReader.cs | Threads DuckDBCpuMetrics through user index reader construction. |
| src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexProcessor.cs | Adds CPU attribution for commit/checkpoint work executed on caller threads. |
| src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexEngineSubscription.cs | Passes DuckDBCpuMetrics through subscription wiring to processors/readers. |
| src/KurrentDB.SecondaryIndexing/Indexes/User/UserIndexEngine.cs | Injects DuckDBCpuMetrics into the user index engine subscription graph. |
| src/KurrentDB.SecondaryIndexing/Indexes/SecondaryIndexReaderBase.cs | Attributes CPU for synchronous DuckDB index-record lookups in read paths. |
| src/KurrentDB.SecondaryIndexing/Indexes/EventType/EventTypeIndexReader.cs | Updates constructor to accept DuckDBCpuMetrics via base class changes. |
| src/KurrentDB.SecondaryIndexing/Indexes/Default/DefaultIndexReader.cs | Updates constructor to accept DuckDBCpuMetrics via base class changes. |
| src/KurrentDB.SecondaryIndexing/Indexes/Default/DefaultIndexProcessor.cs | Adds commit CPU attribution and injects DuckDBCpuMetrics. |
| src/KurrentDB.SecondaryIndexing/Indexes/Category/CategoryIndexReader.cs | Updates constructor to accept DuckDBCpuMetrics via base class changes. |
| src/KurrentDB.SecondaryIndexing.Tests/Indexes/DefaultIndexReaderTests/IndexTestBase.cs | Updates tests to construct readers/processors with DuckDBCpuMetrics. |
| src/KurrentDB.SecondaryIndexing.Tests/Indexes/DefaultIndexProcessorTests.cs | Updates processor test setup for new DuckDBCpuMetrics dependency. |
| src/KurrentDB.SecondaryIndexing.Tests/Diagnostics/DuckDBCpuMetricsTests.cs | Adds tests verifying CPU measurement/tagging for busy vs idle scopes. |
| src/KurrentDB.Core/DuckDB/ThreadCpuTime.cs | Implements platform-specific per-thread CPU time reading. |
| src/KurrentDB.Core/DuckDB/InjectionExtensions.cs | Registers DuckDBCpuMetrics as a singleton created from a dedicated meter. |
| src/KurrentDB.Core/DuckDB/DuckDBCpuMetrics.cs | Defines the meter/counter and the ref-struct scope for caller-side CPU deltas. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
realtonyyoung
left a comment
There was a problem hiding this comment.
One actionable issue I found:
The new query CPU metric stops before the result fetch loop, but every current Flight SQL consumer uses streaming and spends the actual DuckDB result production in IQueryResultReader.TryRead() after ExecuteAsync leaves the measurement scope. That means large/expensive streamed queries can report only snapshot/prepare/initial ExecuteQuery CPU while omitting the per-chunk TryFetch work, and QueryResultReader.Dispose() can also drain remaining chunks without attribution. Please thread DuckDBCpuMetrics into QueryResultReader and measure _result.TryFetch(...) in TryRead()/FinalizeEnumeration(), or otherwise keep the query metric scoped around those synchronous fetch calls too.
The query CPU scope in QueryEngine.ExecuteAsync covered only the synchronous setup (snapshot capture, parse/plan, ExecuteQuery). In streaming mode DuckDB produces result chunks later, inside QueryResultReader.TryRead -> _result.TryFetch, called from ConsumeAsync after that scope closes; FinalizeEnumeration (via Dispose) could also drain chunks unattributed. Large streamed queries therefore under-reported query CPU. Thread DuckDBCpuMetrics into QueryResultReader and measure the synchronous TryFetch calls in TryRead and FinalizeEnumeration under the same query activity. Adds an integration test asserting the streaming read path emits caller-attributed query CPU (tagging/wiring; a per-fetch count assertion is avoided as it would be flaky under the Windows GetThreadTimes quantum). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch — fixed in ae5a03e. You were right that the I threaded Added an integration test ( |
f7ca695 to
ae5a03e
Compare
Deploying eventstore with
|
| Latest commit: |
e537152
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://1491e025.eventstore.pages.dev |
| Branch Preview URL: | https://feat-duckdb-cpu-metric.eventstore.pages.dev |
Heads-up: the two vulnerability checks fail on a repo-wide advisory, not this PR
Per discussion, this is being handled in a dedicated repo-wide PR (suppress the audit + allowlist the scan for this advisory, or migrate to the recommended This PR's own code is green: build clean (0 warnings) and the full |
…ally testable Review fixes for PR #5642: - AddDuckDb resolved the meter's service name via sp.GetService<MetricsConfiguration>(), which is not registered in the collection AddDuckDb runs from, so it always fell back to "kurrentdb" and mis-named the meter under legacy "eventstore" naming. Pass the configured ServiceName in from ClusterVNodeStartup instead, dropping the dead DI lookup. - DuckDBCpuMetrics now takes an optional CPU-time source (defaulting to ThreadCpuTime), so tests can inject a deterministic clock. Replace the timing-dependent streaming-read integration assertion (which could flake when a small query's caller CPU rounded to zero ticks, notably under Windows' quantum-granular GetThreadTimes) with deterministic unit tests that verify the exact recorded delta, tags, and the zero-delta no-op gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # src/KurrentDB.Core/ClusterVNodeStartup.cs
- #5642 is open (not merged): "shipped" -> "proposed", especially since the caller-side metric is being pulled per this spec. - kurrentdb_proc_cpu is an ObservableUpDownCounter (gauge), so rate() is wrong; compare the DuckDB CPU-seconds rate against the gauge directly. - "expected-standard" -> "expected to be standard". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Superseded by the dedicated-executor approach. The caller-side metric fundamentally could not capture DuckDB's parallel worker-thread CPU; it's replaced by the Kurrent.Quack |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
A new
KurrentDB.DuckDBmeter exposingkurrentdb.duckdb.cpu.seconds— a counter of CPU time consumed by DuckDB operations executed on KurrentDB threads, taggedactivity=query|read|commit|checkpointandsource=caller. Today the process-level CPU metric is one opaque number; this is the first step toward decomposing it into "KurrentDB vs DuckDB" on all supported platforms, e.g.:How
ThreadCpuTime(KurrentDB.Core/DuckDB): cumulative CPU time of the calling thread —clock_gettime(CLOCK_THREAD_CPUTIME_ID)on Linux (clock id 3) and macOS (16),GetThreadTimes(GetCurrentThread())on Windows. No-ops on unsupported platforms.DuckDBCpuMetrics.Measure(activity)returns aref structscope, so the compiler rejects any use across anawait— per-thread CPU deltas are only valid when start and stop happen on the same thread.SecondaryIndexReaderBase), andQueryEngine's snapshot-capture/parse/plan/execute section.AddDuckDb()and listed inmetricsconfig.json, so it exports through the standard/metricsendpoint with no extra wiring.Verification
CommitBatchSize=1, one event written):Known limits
ConsumeAsync); that share is invisible from the caller side and is planned assource=workersvia Quack owning DuckDB's workers (threads=N, external_threads=N+duckdb_execute_tasks_state) — which also yields a CPU cap. Until then this metric is a lower bound, tightest for commit/checkpoint/read activities which are fully synchronous.🤖 Generated with Claude Code