From 40284b006187de062fe300a3d12a2065c029cdbf Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:41:48 -0400 Subject: [PATCH] Bucket the Darling viewer's TempDB file I/O trend (#4234) TempDbFileIoTrendSql returned one row per (collection_time, file_name) for a tempdb file, unbounded on the window end, unlike the File I/O tab's own reads (#4329) and Lite's already-bucketed twin (#4340). A 7-day window at 1-minute cadence for 4 files measured 40,324 rows on a throwaway rig seed; the same seed through the new date_bin bucket (auto-picked to 10 minutes for that window) measures 4,036. GetTempDbFileIoTrendAsync now takes (serverId, startUtc, endUtc) like the other bucketed trend reads, computes its bucket width from TrendBuckets.AutoMinutes, and stamps points at their own raw collection time instead of the bucket grid when every returned bucket is a singleton. The #3540 "no delta knowable" marker is carried into a rated CTE (nulled, not filtered) so collection_count still counts an unrated row and a bucket is never mistaken for a true singleton. LoadTempDbAsync passes endUtc through and drops the now-redundant client-side post-filter on the file-I/O series. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 --- Darling/Darling.Tests/ViewerCpuTempDbTests.cs | 120 +++++++++++++++-- .../ViewerFileIoBlockingTests.cs | 10 +- .../ViewerDataService.TempDb.cs | 125 +++++++++++++----- .../ViewerServerTab.Charts.cs | 6 +- 4 files changed, 210 insertions(+), 51 deletions(-) diff --git a/Darling/Darling.Tests/ViewerCpuTempDbTests.cs b/Darling/Darling.Tests/ViewerCpuTempDbTests.cs index ddfa554331..e79b4c924f 100644 --- a/Darling/Darling.Tests/ViewerCpuTempDbTests.cs +++ b/Darling/Darling.Tests/ViewerCpuTempDbTests.cs @@ -25,10 +25,11 @@ namespace Darling.Tests; /// per-sample series bucketed to 's width (#4234): a bucket wider than /// one sample averages the two gauge columns, and a bucket holding exactly one physical sample is /// stamped at that sample's own raw time by the C# reader rather than the bucket grid (ruling item 3). -/// The tempdb reads mirror Lite's view-based queries (v_tempdb_stats / v_file_io_stats), unaffected by -/// #4234: the numeric(18,2) MB columns are CAST to double precision for the typed reader, -/// total_sessions_using_tempdb stays bigint (GetInt64), and the file-I/O read filters to tempdb and -/// averages stall/op per file. +/// The tempdb reads mirror Lite's view-based queries (v_tempdb_stats / v_file_io_stats): the numeric(18,2) +/// MB columns are CAST to double precision for the typed reader, total_sessions_using_tempdb stays bigint +/// (GetInt64). The usage read (TempDbTrendSql) is unaffected by #4234 — still one row per collection. The +/// file-I/O read filters to tempdb, averages stall/op per file, and — since #4234 — is bucketed the same +/// way as the File I/O tab's own per-file reads (Lite's twin bucketed first, #4340). /// public sealed class ViewerCpuTempDbSqlTests { @@ -153,18 +154,37 @@ public void TempDbTrendSql_ReadsColumnsThatExistInTheGeneratedTempDbTable() } [Fact] - public void TempDbFileIoTrendSql_FiltersToTempDb_GroupsPerFile_CastsStallToDouble() + public void TempDbFileIoTrendSql_FiltersToTempDb_GroupsPerFileAndBucket_CastsStallToDouble() { Assert.Contains("FROM v_file_io_stats", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); Assert.Contains("WHERE server_id = $1", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); Assert.Contains("collection_time >= $2", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); + Assert.Contains("collection_time <= $3", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); Assert.Contains("database_name = 'tempdb'", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); - Assert.Contains("GROUP BY collection_time, file_name", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); - Assert.Contains("ORDER BY collection_time, file_name", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); + /* #4234: grouped on file_name plus the bucket_start ordinal, not the raw collection_time column + (the pre-bucketing read grouped/ordered on the bare column). */ + Assert.Contains("GROUP BY file_name, 2", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); + Assert.Contains("ORDER BY file_name, 2", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); Assert.Contains("CAST(delta_stall_read_ms AS double precision)", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); Assert.Contains("CAST(delta_stall_write_ms AS double precision)", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); } + /// #4234 ruling item 6 (source check): the tempdb file-I/O read carries a bucket width and + /// projects the singleton-detection columns, mirroring FileIoLatencyTrendSql. Proven once by + /// hand against the pre-#4234 text: no date_bin anywhere (a per-collection read has no bucket + /// width), so this assert fails there. + [Fact] + public void TempDbFileIoTrendSql_CarriesABucketWidth_AndProjectsSingletonDetectionColumns() + { + var sql = ViewerDataService.TempDbFileIoTrendSql; + Assert.Contains("date_bin(CAST($4 AS integer) * INTERVAL '1 minute'", sql, StringComparison.Ordinal); + Assert.Contains(TrendBucketSql.OriginSql, sql, StringComparison.Ordinal); + Assert.Contains("GREATEST(date_bin(", sql, StringComparison.Ordinal); + Assert.Contains("MIN(collection_time) AS first_collection_time", sql, StringComparison.Ordinal); + Assert.Contains("COUNT(*) AS collection_count", sql, StringComparison.Ordinal); + Assert.Contains("HAVING COUNT(rated_reads) > 0", sql, StringComparison.Ordinal); + } + [Fact] public void TempDbFileIoTrendSql_ReadsColumnsThatExistInTheGeneratedFileIoTable() { @@ -208,9 +228,10 @@ public void NewViewerReads_PgDialect_PositionalParams_NoBareNow_NoNLiterals(stri /// naive UTC by the per-batch offset, and a UTC batch left untouched, both landing on collection_time); /// the tempdb usage read (MB-as-double and — the load-bearing check — the bigint /// total_sessions_using_tempdb read via GetInt64, which GetInt32 would throw on); and the tempdb -/// file-I/O read (tempdb-only filtering + per-file average-latency computation). Shares the serialized -/// "live-postgres" collection so the row churn can't race another class; uses negative sentinel -/// server_ids and cleans up in finally. +/// file-I/O read (tempdb-only filtering, per-file average-latency computation, and — since #4234 — the +/// same per-file bucket cap and singleton pass-through as the File I/O tab's own reads). Shares the +/// serialized "live-postgres" collection so the row churn can't race another class; uses negative +/// sentinel server_ids and cleans up in finally. /// [Collection("live-postgres")] public sealed class ViewerCpuTempDbLivePostgresTests @@ -230,6 +251,8 @@ public sealed class ViewerCpuTempDbLivePostgresTests private const int FileIoServerId = -969696; private const string FileIoServerName = "viewer-tempdb-fileio-e2e"; + private const int FileIoBudgetServerId = -969697; + [Fact] public async Task Cpu_ReadsRawSamplesInOrder_NullOtherAsZero_AgainstDevPostgres() { @@ -534,6 +557,10 @@ await LiveStoreCleanup.RunAsync(connectionString!, bodySucceeded, async (cleanup } } + /// #4234 ruling item 3: at a short window the budget covers every collection (one per file + /// here), so both reads return the raw points UNCHANGED, stamped at their own raw collection time + /// rather than the date_bin grid — proven by the exact-tick equality below, which the always-floors + /// pre-#4234 read also happened to satisfy only because it never bucketed at all. [Fact] public async Task TempDbFileIo_TempDbOnly_PerFileAverageLatency_AgainstDevPostgres() { @@ -561,10 +588,12 @@ await InsertFileIoAsync(connection, t1, "tempdb", "tempdb_log", await InsertFileIoAsync(connection, t1, "StackOverflow2010", "so_data", deltaReads: 1000, deltaWrites: 1000, deltaStallReadMs: 999999, deltaStallWriteMs: 999999); - var rows = await viewer.GetTempDbFileIoTrendAsync(FileIoServerId, t1.AddMinutes(-1)); + var rows = await viewer.GetTempDbFileIoTrendAsync(FileIoServerId, t1.AddMinutes(-1), t1.AddMinutes(1)); /* Only the two tempdb files survive the database_name = 'tempdb' filter. */ Assert.Equal(2, rows.Count); + /* #4234: one collection per file over the whole window — the budget covers it, so every + point is stamped at its own raw collection_time, not a bucket_start grid line. */ Assert.All(rows, r => Assert.Equal(t1.Ticks, r.CollectionTime.Ticks)); /* Ordered by file_name. */ Assert.Equal(new[] { "tempdb_data", "tempdb_log" }, rows.Select(r => r.FileName)); @@ -585,6 +614,51 @@ await LiveStoreCleanup.RunAsync(connectionString!, bodySucceeded, async (cleanup } } + /// #4234 ruling items 2/6: a 7-day window at the real 1-minute file_io_stats cadence used to + /// return every collection per file — now capped to 's point budget PER + /// FILE, exactly like the File I/O tab's own per-file reads. Bulk-seeded server-side (generate_series) + /// rather than one round trip per minute. + [Fact] + public async Task TempDbFileIo_SevenDayWindow_ReturnsAtMostBudgetTimesFileCount_AgainstDevPostgres() + { + var connectionString = Environment.GetEnvironmentVariable("DARLING_TEST_PG"); + Assert.SkipWhen(string.IsNullOrEmpty(connectionString), + "Set DARLING_TEST_PG to a Postgres connection string to run the live tempdb-file-I/O budget-cap test."); + + using var connection = new NpgsqlConnection(connectionString); + await connection.OpenAsync(TestContext.Current.CancellationToken); + await PgMigrations.MigrateAsync(connection, TestContext.Current.CancellationToken); + await DeleteRowsAsync(connection, "file_io_stats", FileIoBudgetServerId, TestContext.Current.CancellationToken); + + await using var viewer = new ViewerDataService(connectionString!); + + var bodySucceeded = false; + try + { + var end = new DateTime(2026, 3, 10, 0, 0, 0); + var start = end.AddDays(-7); + var files = new[] { "tempdb_data", "tempdb_data2", "tempdb_log", "tempdb_data3" }; + var baseId = CollectionIdGenerator.Next() * 1_000_000L; + foreach (var file in files) + { + await BulkSeedTempDbFileIoAsync(connection, TestContext.Current.CancellationToken, baseId, FileIoBudgetServerId, start, end, file); + baseId += 20_000; + } + + var rows = await viewer.GetTempDbFileIoTrendAsync(FileIoBudgetServerId, start, end); + var budget = TrendBudget.Chart.AutoPoints * files.Length; + + Assert.True(rows.Count > 0 && rows.Count <= budget, $"tempdb file I/O: {rows.Count} rows over a {files.Length}-file budget of {budget}"); + + bodySucceeded = true; + } + finally + { + await LiveStoreCleanup.RunAsync(connectionString!, bodySucceeded, async (cleanup, cleanupCt) => + await DeleteRowsAsync(cleanup, "file_io_stats", FileIoBudgetServerId, cleanupCt)); + } + } + private static async Task InsertCpuAsync( NpgsqlConnection connection, int serverId, string serverName, DateTime collectionTimeUtc, DateTime sampleTimeLocal, int sqlCpu, int? otherCpu) @@ -677,6 +751,30 @@ INSERT INTO file_io_stats await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } + /// One tempdb file_io_stats collection per minute from to + /// inclusive, generated server-side — the real cadence a 7-day tempdb-file-I/O + /// window would see — without a per-row round trip. Mirrors ViewerFileIoBlockingTests's + /// BulkSeedFileIoAsync, database_name fixed to 'tempdb'. + private static async Task BulkSeedTempDbFileIoAsync( + NpgsqlConnection connection, System.Threading.CancellationToken ct, long baseId, int serverId, + DateTime start, DateTime end, string fileName) + { + using var command = new NpgsqlCommand(@" +INSERT INTO file_io_stats + (collection_id, collection_time, server_id, server_name, + database_name, file_name, delta_reads, delta_writes, + delta_stall_read_ms, delta_stall_write_ms) +SELECT $1 + row_number() OVER (), g, $2, $3, 'tempdb', $4, 10, 4, 100, 20 +FROM generate_series($5::timestamp, $6::timestamp, interval '1 minute') AS g", connection); + command.Parameters.AddWithValue(baseId); + command.Parameters.AddWithValue(serverId); + command.Parameters.AddWithValue(FileIoServerName); + command.Parameters.AddWithValue(fileName); + command.Parameters.AddWithValue(DateTime.SpecifyKind(start, DateTimeKind.Unspecified)); + command.Parameters.AddWithValue(DateTime.SpecifyKind(end, DateTimeKind.Unspecified)); + await command.ExecuteNonQueryAsync(ct); + } + private static DateTime TruncateToSeconds(DateTime value) => DateTime.SpecifyKind(new DateTime(value.Ticks - (value.Ticks % TimeSpan.TicksPerSecond)), DateTimeKind.Unspecified); diff --git a/Darling/Darling.Tests/ViewerFileIoBlockingTests.cs b/Darling/Darling.Tests/ViewerFileIoBlockingTests.cs index 0bbc78b41d..0b6526f5f9 100644 --- a/Darling/Darling.Tests/ViewerFileIoBlockingTests.cs +++ b/Darling/Darling.Tests/ViewerFileIoBlockingTests.cs @@ -146,14 +146,16 @@ public void LockWaitTrendSql_FiltersLckPrefix_PerSecondRateViaLag() Assert.DoesNotContain("ELSE 0", ViewerDataService.LockWaitTrendSql, StringComparison.Ordinal); } - /// #3540: the latency reads drop rows whose stored interval is 0 — the calculator's "no delta - /// knowable" marker — so a restart renders as an absent point, never "0.00 ms". IS DISTINCT FROM 0 keeps - /// pre-V127 rows (NULL). Both the File I/O tab's read and the tempdb tab's file read. + /// #3540: the latency reads null out rows whose stored interval is 0 — the calculator's "no + /// delta knowable" marker — so a restart renders as an absent point, never "0.00 ms". IS DISTINCT FROM 0 + /// keeps pre-V127 rows (NULL). Both the File I/O tab's read and the tempdb tab's file read (#4234: + /// bucketed, so both null the marker out of a rated CTE rather than filtering the row out of the + /// FROM clause — the row still counts toward collection_count). [Fact] public void FileIoLatencyReads_DropTheUnknowableMarker_KeepPreV127Rows() { Assert.Contains("CASE WHEN f.sample_interval_seconds IS DISTINCT FROM 0 THEN f.delta_reads END AS rated_reads", ViewerDataService.FileIoLatencyTrendSql, StringComparison.Ordinal); - Assert.Contains("AND sample_interval_seconds IS DISTINCT FROM 0", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); + Assert.Contains("CASE WHEN sample_interval_seconds IS DISTINCT FROM 0 THEN delta_reads END AS rated_reads", ViewerDataService.TempDbFileIoTrendSql, StringComparison.Ordinal); } [Fact] diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.TempDb.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.TempDb.cs index 14fb898963..5f5fc05b5a 100644 --- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.TempDb.cs +++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.TempDb.cs @@ -12,6 +12,9 @@ using System.Threading.Tasks; using Npgsql; +using PerformanceMonitor.Common; +using PerformanceMonitor.Darling.Storage; + namespace PerformanceMonitor.Darling.Viewer; /// @@ -34,9 +37,13 @@ public sealed record TempDbSample( double TopSessionTempDbMb); /// -/// One tempdb file's I/O-latency point — a collection time, the file name, and the read/write average -/// latencies (ms) for that collection. Mirror of Lite's FileIoTrendPoint as the tempdb file-I/O -/// chart consumes it; the chart sums read+write latency per file and cycles a color per file. +/// One tempdb file's I/O-latency point — a bucket (or, for a singleton bucket, a raw collection) time, +/// the file name, and the read/write average latencies (ms). Mirror of Lite's FileIoTrendPoint as +/// the tempdb file-I/O chart consumes it; the chart sums read+write latency per file and cycles a color +/// per file. +/// #4234: CollectionTime is a date_bin bucket start, EXCEPT when every bucket the call +/// returned holds exactly one physical collection, in which case every point is stamped at its own raw +/// collection time instead — see . /// public sealed record TempDbFileIoSample( DateTime CollectionTime, @@ -73,32 +80,52 @@ ORDER BY collection_time """; /// - /// The tempdb per-file I/O-latency read — Lite's GetTempDbFileIoTrendAsync ported to - /// Postgres. Reads v_file_io_stats (a SELECT * passthrough over file_io_stats) - /// to mirror Lite's view-based query, filtered to tempdb, and computes per-collection average - /// read/write latency (stall ms / operation count) with the delta-stall sums CAST to double - /// precision before division. Grouped by (collection_time, file_name) so each tempdb data file is - /// its own series. Rows whose stored sample_interval_seconds is 0 — no delta knowable, a - /// restart — are dropped rather than rendered as 0.00 ms (#3540). - /// $1 server_id, $2 window start (naive UTC). + /// The tempdb per-file I/O-latency read — Lite's GetTempDbFileIoTrendAsync ported to Postgres, + /// then bucketed (#4234), matching Lite's own bucketed twin (LocalDataService.FileIo.cs, + /// #4340) column-for-column. Reads v_file_io_stats (a SELECT * passthrough over + /// file_io_stats) to mirror Lite's view-based query, filtered to tempdb, and computes average + /// read/write latency per bucket (stall ms / operation count) with the delta-stall sums CAST to + /// double precision before division. Grouped by file_name so each tempdb data file is its own series + /// — no top-10 ranking pass, unlike , since tempdb's own file count + /// is already small. + /// #3540, carried into the bucket exactly like : a row whose + /// stored sample_interval_seconds is 0 (no delta knowable — first sighting, counter reset, a + /// gap past the policy) has its deltas nulled out of the sums via the rated CTE rather than + /// contributing a confident 0, exactly as the pre-bucket read dropped the row outright. IS DISTINCT + /// FROM 0 still keeps pre-V127 rows (NULL: interval never recorded). The row is NOT removed from + /// the FROM clause — it still counts toward collection_count below, so a bucket is never + /// mistaken for a true singleton just because its one physical collection happened to be unrated. + /// HAVING COUNT(rated_reads) > 0 drops a bucket whose every row was unrated, matching the + /// pre-bucket read's "absent, not 0.00 ms" answer for that case. + /// $1 server_id, $2 window start, $3 window end (naive UTC), $4 bucket width minutes. /// - public const string TempDbFileIoTrendSql = """ + public const string TempDbFileIoTrendSql = $$""" + WITH rated AS ( + SELECT + file_name, + collection_time, + /* #3540: see FileIoLatencyTrendSql's rated CTE for why this nulls rather than filters. */ + CASE WHEN sample_interval_seconds IS DISTINCT FROM 0 THEN delta_reads END AS rated_reads, + CASE WHEN sample_interval_seconds IS DISTINCT FROM 0 THEN delta_writes END AS rated_writes, + CASE WHEN sample_interval_seconds IS DISTINCT FROM 0 THEN CAST(delta_stall_read_ms AS double precision) END AS rated_stall_read_ms, + CASE WHEN sample_interval_seconds IS DISTINCT FROM 0 THEN CAST(delta_stall_write_ms AS double precision) END AS rated_stall_write_ms + FROM v_file_io_stats + WHERE server_id = $1 + AND collection_time >= $2 + AND collection_time <= $3 + AND database_name = 'tempdb' + ) SELECT - collection_time, file_name, - CASE WHEN SUM(delta_reads) > 0 THEN SUM(CAST(delta_stall_read_ms AS double precision)) / SUM(delta_reads) ELSE 0 END AS avg_read_latency_ms, - CASE WHEN SUM(delta_writes) > 0 THEN SUM(CAST(delta_stall_write_ms AS double precision)) / SUM(delta_writes) ELSE 0 END AS avg_write_latency_ms - FROM v_file_io_stats - WHERE server_id = $1 - AND collection_time >= $2 - AND database_name = 'tempdb' - /* #3540: a stored interval of 0 is the calculator's "no delta knowable" marker (first sighting, - counter reset, a gap past the policy) — the row is dropped so the point is ABSENT rather than the - confident "0.00 ms" a restart used to render. IS DISTINCT FROM 0 keeps pre-V127 rows (NULL: interval - never recorded), which carry on reading exactly as they always did. */ - AND sample_interval_seconds IS DISTINCT FROM 0 - GROUP BY collection_time, file_name - ORDER BY collection_time, file_name + GREATEST(date_bin(CAST($4 AS integer) * INTERVAL '1 minute', collection_time, {{TrendBucketSql.OriginSql}}), $2) AS bucket_start, + CASE WHEN SUM(rated_reads) > 0 THEN SUM(rated_stall_read_ms) / SUM(rated_reads) ELSE 0 END AS avg_read_latency_ms, + CASE WHEN SUM(rated_writes) > 0 THEN SUM(rated_stall_write_ms) / SUM(rated_writes) ELSE 0 END AS avg_write_latency_ms, + MIN(collection_time) AS first_collection_time, + COUNT(*) AS collection_count + FROM rated + GROUP BY file_name, 2 + HAVING COUNT(rated_reads) > 0 + ORDER BY file_name, 2 """; /// @@ -136,30 +163,62 @@ public async Task> GetTempDbTrendAsync(int serverId, DateTime } /// - /// tempdb per-file I/O-latency points for one server since , - /// time- then file-ordered, for the tempdb tab's file-I/O chart. + /// tempdb per-file I/O-latency points for one server over the window, file- then bucket-ordered, for + /// the tempdb tab's file-I/O chart. + /// #4234: buckets to 's point budget, exactly like + /// — see its remarks for the width and singleton-stamping + /// rules, which this read shares verbatim ( replaces the old start-only + /// window so the SQL can bucket on a known width). /// - public async Task> GetTempDbFileIoTrendAsync(int serverId, DateTime sinceUtc, CancellationToken cancellationToken = default) + public async Task> GetTempDbFileIoTrendAsync( + int serverId, DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken = default) { var samples = new List(); + var windowMinutes = Math.Max(1, (int)Math.Ceiling((endUtc - startUtc).TotalMinutes)); + var bucketMinutes = TrendBuckets.AutoMinutes(windowMinutes, 1, TrendBudget.Chart.AutoPoints); + await using var command = _dataSource.CreateCommand(TempDbFileIoTrendSql); command.CommandTimeout = ViewerCommandDeadlines.CurrentInteractiveReadSeconds; command.Parameters.Add(new NpgsqlParameter { TypedValue = serverId }); command.Parameters.Add(new NpgsqlParameter { - TypedValue = DateTime.SpecifyKind(sinceUtc, DateTimeKind.Unspecified), + TypedValue = DateTime.SpecifyKind(startUtc, DateTimeKind.Unspecified), + }); + command.Parameters.Add(new NpgsqlParameter + { + TypedValue = DateTime.SpecifyKind(endUtc, DateTimeKind.Unspecified), }); + command.Parameters.Add(new NpgsqlParameter { TypedValue = bucketMinutes }); + + var rows = new List<(string FileName, DateTime BucketStart, DateTime FirstCollectionTime, double AvgRead, double AvgWrite)>(); + var everyBucketSingleton = true; + await using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { - samples.Add(new TempDbFileIoSample( - reader.GetDateTime(0), - reader.IsDBNull(1) ? "" : reader.GetString(1), + if (reader.GetInt64(5) != 1) + { + everyBucketSingleton = false; + } + + rows.Add(( + reader.IsDBNull(0) ? "" : reader.GetString(0), + reader.GetDateTime(1), + reader.GetDateTime(4), reader.IsDBNull(2) ? 0 : reader.GetDouble(2), reader.IsDBNull(3) ? 0 : reader.GetDouble(3))); } + foreach (var row in rows) + { + samples.Add(new TempDbFileIoSample( + everyBucketSingleton ? row.FirstCollectionTime : row.BucketStart, + row.FileName, + row.AvgRead, + row.AvgWrite)); + } + return samples; } } diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Charts.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Charts.cs index 66dfcbb3ea..6579100497 100644 --- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Charts.cs +++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Charts.cs @@ -121,15 +121,15 @@ private async Task LoadTempDbAsync() /* Both reads run concurrently — NpgsqlDataSource pools a connection for each. */ using var readFanOut = ViewerReadFanOut.Of(2); var trendTask = _dataService.GetTempDbTrendAsync(_server.ServerId, startUtc); - var fileIoTask = _dataService.GetTempDbFileIoTrendAsync(_server.ServerId, startUtc); + var fileIoTask = _dataService.GetTempDbFileIoTrendAsync(_server.ServerId, startUtc, endUtc); var trend = await trendTask; var fileIo = await fileIoTask; - /* Start-only reads; bound the end for a custom range (collection_time is naive UTC). */ + /* trend is a start-only read; bound the end for a custom range (collection_time is naive UTC). + fileIo is bucketed (#4234) and already windowed server-side on endUtc. */ if (IsCustomRange) { trend = trend.Where(t => t.CollectionTime <= endUtc).ToList(); - fileIo = fileIo.Where(f => f.CollectionTime <= endUtc).ToList(); } RenderTempDbUsageChart(trend, startUtc, endUtc);