-
Notifications
You must be signed in to change notification settings - Fork 679
Add caller-side DuckDB CPU attribution metric #5642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2af62e3
Add caller-side DuckDB CPU attribution metric
realtonyyoung ae5a03e
Attribute streaming result-fetch CPU in DuckDB query metric
realtonyyoung 650cfe1
Fix DuckDB CPU meter service name; make CPU measurement deterministic…
realtonyyoung e537152
Merge remote-tracking branch 'origin/master' into feat/duckdb-cpu-metric
realtonyyoung File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements. | ||
| // Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md). | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Diagnostics.Metrics; | ||
|
|
||
| namespace KurrentDB.Core.DuckDB; | ||
|
|
||
| // Attributes CPU consumed by DuckDB operations executed on KurrentDB threads. | ||
| // DuckDB also schedules work onto its own native worker threads; that share is not visible | ||
| // to caller-side measurement and is reported separately once KurrentDB owns those workers. | ||
| public class DuckDBCpuMetrics { | ||
| public const string MeterName = "KurrentDB.DuckDB"; | ||
|
|
||
| public static class Activities { | ||
| public const string Query = "query"; | ||
| public const string Read = "read"; | ||
| public const string Commit = "commit"; | ||
| public const string Checkpoint = "checkpoint"; | ||
| } | ||
|
|
||
| private static readonly KeyValuePair<string, object> SourceTag = new("source", "caller"); | ||
|
|
||
| private readonly Counter<double> _cpuSeconds; | ||
|
|
||
| public DuckDBCpuMetrics(Meter meter, string serviceName) { | ||
| _cpuSeconds = meter.CreateCounter<double>( | ||
| $"{serviceName}.duckdb.cpu.seconds", | ||
| description: "CPU time consumed by DuckDB operations on KurrentDB threads, in seconds"); | ||
| } | ||
|
|
||
| public CpuScope Measure(string activity) => new(this, activity); | ||
|
|
||
| // A ref struct so it cannot live across an await: the CPU delta is only valid when start | ||
| // and stop are read on the same thread. | ||
| public readonly ref struct CpuScope { | ||
| private readonly DuckDBCpuMetrics _metrics; | ||
| private readonly string _activity; | ||
| private readonly long _startNanoseconds; | ||
|
|
||
| internal CpuScope(DuckDBCpuMetrics metrics, string activity) { | ||
| if (!ThreadCpuTime.IsSupported) | ||
| return; | ||
|
|
||
| _metrics = metrics; | ||
| _activity = activity; | ||
| _startNanoseconds = ThreadCpuTime.CurrentNanoseconds; | ||
| } | ||
|
|
||
| public void Dispose() { | ||
| if (_metrics is null) | ||
| return; | ||
|
|
||
| var elapsedNanoseconds = ThreadCpuTime.CurrentNanoseconds - _startNanoseconds; | ||
| if (elapsedNanoseconds > 0) | ||
| _metrics._cpuSeconds.Add( | ||
| elapsedNanoseconds / 1e9, | ||
| new KeyValuePair<string, object>("activity", _activity), | ||
| SourceTag); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements. | ||
| // Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md). | ||
|
|
||
| using System; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace KurrentDB.Core.DuckDB; | ||
|
|
||
| // Reads the cumulative CPU time consumed by the calling thread. | ||
| // A delta between two readings is only meaningful when both are taken on the same thread. | ||
| internal static class ThreadCpuTime { | ||
| // CLOCK_THREAD_CPUTIME_ID differs per libc: bits/time.h on Linux, sys/_types/_clockid_t on macOS | ||
| private const int ClockThreadCpuTimeIdLinux = 3; | ||
| private const int ClockThreadCpuTimeIdMacOS = 16; | ||
|
|
||
| public static readonly bool IsSupported = Detect(); | ||
|
|
||
| public static long CurrentNanoseconds => OperatingSystem.IsWindows() | ||
| ? GetWindowsThreadCpuNanoseconds() | ||
| : GetPosixThreadCpuNanoseconds(); | ||
|
|
||
| private static long GetPosixThreadCpuNanoseconds() { | ||
| var clockId = OperatingSystem.IsLinux() ? ClockThreadCpuTimeIdLinux : ClockThreadCpuTimeIdMacOS; | ||
| return clock_gettime(clockId, out var ts) == 0 | ||
| ? ts.Seconds * 1_000_000_000L + ts.Nanoseconds | ||
| : 0; | ||
| } | ||
|
|
||
| private static long GetWindowsThreadCpuNanoseconds() { | ||
| // GetThreadTimes reports in 100ns units, accrued at scheduler-quantum granularity. | ||
| // Cumulative totals are statistically accurate; very short individual deltas are not. | ||
| return GetThreadTimes(GetCurrentThread(), out _, out _, out var kernelTime, out var userTime) | ||
| ? (kernelTime + userTime) * 100 | ||
| : 0; | ||
| } | ||
|
|
||
| private static bool Detect() { | ||
| if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) | ||
| return false; | ||
|
|
||
| try { | ||
| _ = CurrentNanoseconds; | ||
| return true; | ||
| } catch (Exception e) when (e is DllNotFoundException or EntryPointNotFoundException) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| private struct Timespec { | ||
| public nint Seconds; | ||
| public nint Nanoseconds; | ||
| } | ||
|
|
||
| [DllImport("libc")] | ||
| private static extern int clock_gettime(int clockId, out Timespec ts); | ||
|
|
||
| [DllImport("kernel32.dll", ExactSpelling = true)] | ||
| private static extern nint GetCurrentThread(); | ||
|
|
||
| [DllImport("kernel32.dll", ExactSpelling = true)] | ||
| private static extern bool GetThreadTimes(nint thread, out long creationTime, out long exitTime, out long kernelTime, out long userTime); | ||
| } |
58 changes: 58 additions & 0 deletions
58
src/KurrentDB.SecondaryIndexing.Tests/Diagnostics/DuckDBCpuMetricsTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements. | ||
| // Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md). | ||
|
|
||
| using System.Diagnostics; | ||
| using System.Diagnostics.Metrics; | ||
| using KurrentDB.Core.DuckDB; | ||
|
|
||
| namespace KurrentDB.SecondaryIndexing.Tests.Diagnostics; | ||
|
|
||
| public class DuckDBCpuMetricsTests { | ||
| [Fact] | ||
| public void busy_scope_records_cpu_seconds_with_activity_and_source_tags() { | ||
| using var meter = new Meter("test"); | ||
| var metrics = new DuckDBCpuMetrics(meter, "kurrentdb"); | ||
|
|
||
| List<(double Value, KeyValuePair<string, object?>[] Tags)> measurements = []; | ||
| using var listener = Listen(meter, measurements); | ||
|
|
||
| using (metrics.Measure(DuckDBCpuMetrics.Activities.Commit)) { | ||
| // busy spin long enough to accrue CPU past Windows' quantum-granular thread accounting | ||
| var stopwatch = Stopwatch.StartNew(); | ||
| while (stopwatch.ElapsedMilliseconds < 100) { | ||
| } | ||
| } | ||
|
|
||
| var measurement = Assert.Single(measurements); | ||
| Assert.InRange(measurement.Value, 0.001, 30); | ||
| Assert.Contains(measurement.Tags, t => t is { Key: "activity", Value: "commit" }); | ||
| Assert.Contains(measurement.Tags, t => t is { Key: "source", Value: "caller" }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void idle_scope_records_at_most_negligible_cpu() { | ||
| using var meter = new Meter("test"); | ||
| var metrics = new DuckDBCpuMetrics(meter, "kurrentdb"); | ||
|
|
||
| List<(double Value, KeyValuePair<string, object?>[] Tags)> measurements = []; | ||
| using var listener = Listen(meter, measurements); | ||
|
|
||
| using (metrics.Measure(DuckDBCpuMetrics.Activities.Read)) { | ||
| Thread.Sleep(100); | ||
| } | ||
|
|
||
| Assert.True(measurements.Sum(m => m.Value) < 0.05, $"expected near-zero CPU, got {measurements.Sum(m => m.Value)}s"); | ||
| } | ||
|
|
||
| private static MeterListener Listen(Meter meter, List<(double, KeyValuePair<string, object?>[])> measurements) { | ||
| var listener = new MeterListener(); | ||
| listener.InstrumentPublished = (instrument, l) => { | ||
| if (instrument.Meter == meter && instrument.Name == "kurrentdb.duckdb.cpu.seconds") | ||
| l.EnableMeasurementEvents(instrument); | ||
| }; | ||
| listener.SetMeasurementEventCallback<double>( | ||
| (_, value, tags, _) => measurements.Add((value, tags.ToArray()))); | ||
| listener.Start(); | ||
| return listener; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.