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);