Skip to content

Commit f12072c

Browse files
Rollup reads stop at the window end instead of adding the hour or day that starts there (#4859)
A rollup bucket is stamped at its start. Nine reads over the hourly rollups, and the daily one for Custom Views, bounded the window end with bucket <= end. A window that ended exactly on a bucket start also took the whole bucket that begins there, which lies after the window. A Queries tab range older than raw retention whose To was 14:00 summed the 14:00-15:00 hour too. A Custom View on the daily tier that ended at midnight summed the next day. - These reads now stop before the window end. They are the Queries and Procedures tabs' hourly tier and the top queries and top procedures tools' hourly reads. The hourly and bucketed hourly duration trends and the Query Store trend's rollup arm also stop there. So do one query's hourly history and Custom Views on a rollup route. - A window end inside a bucket reads as before, with that bucket counted whole. Raw reads keep their inclusive end, because a raw sample is stamped when it was taken. - The served-span note no longer claims the next hour for an end exactly on the hour. - The first-bucket probes keep their inclusive end on purpose. They only locate where the served span starts.
1 parent 6913775 commit f12072c

15 files changed

Lines changed: 732 additions & 67 deletions

‎Darling/Darling.Tests/DarlingMcpTrendToolsTests.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,10 @@ public void DurationTrendHourlySql_ReadsTheRollup_BucketsByIt_ProjectsTheSharedS
415415
Assert.DoesNotContain("FROM " + rawTable + "\n", sql, StringComparison.Ordinal);
416416
Assert.DoesNotContain("collection_time >=", sql, StringComparison.Ordinal);
417417
Assert.Contains("bucket >= $2", sql, StringComparison.Ordinal);
418-
Assert.Contains("bucket <= $3", sql, StringComparison.Ordinal);
418+
/* A bucket is stamped at its START, so the window end is exclusive: `bucket <= $3` would take the whole
419+
hour that begins at an end falling on the hour (RollupWindowEndBoundTests pins the same for every rollup read). */
420+
Assert.Contains("bucket < $3", sql, StringComparison.Ordinal);
421+
Assert.DoesNotContain("bucket <= $3", sql, StringComparison.Ordinal);
419422
Assert.Contains("GROUP BY bucket", sql, StringComparison.Ordinal);
420423

421424
/* Since #3897 the rollup's hours are gathered into $4-minute buckets on the shared origin; the columns and

‎Darling/Darling.Tests/HourlyAttributionSpanTests.cs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ public void Hourly_ShareDividesByTheServedSpan_NotTheRequestedWindow()
4848
Assert.NotEqual(served.AttributedCpuRatio, requested.AttributedCpuRatio);
4949
}
5050

51+
[Fact]
52+
public void Hourly_OnTheHourAsOf_DividesByExactlyTheHoursBeforeIt()
53+
{
54+
// An on-the-hour as_of divides N hours of CPU by N hours, not N+1 (#4859): the read stops before the hour
55+
// that begins at as_of, so that hour is not in the denominator. The ceiling sits above as_of, so it does
56+
// not cut the span.
57+
var start = Day.AddHours(10);
58+
var asOf = Day.AddHours(14);
59+
var (from, to, _) = DarlingMcpDataTools.HourlyAttributionSpan(true, start, asOf, Day.AddHours(10), Day.AddHours(18));
60+
61+
Assert.Equal(Day.AddHours(10), from);
62+
Assert.Equal(Day.AddHours(14), to);
63+
64+
// 25% of 8 cores over exactly 4 hours = 28,800 CPU-seconds; 21,600 ranked seconds is 0.75.
65+
var served = CpuAttribution.Compute(21600, from, to, 240, from, to, 25, 8);
66+
Assert.Equal(28800, served.SqlCpuSecondsInWindow);
67+
Assert.Equal(0.75, served.AttributedCpuRatio);
68+
69+
// Contrast: an as_of 20 minutes into the hour still counts the bucket holding it whole, so the span ends
70+
// at the top of the next hour.
71+
var (_, unalignedTo, _) = DarlingMcpDataTools.HourlyAttributionSpan(
72+
true, start, Day.AddHours(14).AddMinutes(20), Day.AddHours(10), Day.AddHours(18));
73+
Assert.Equal(Day.AddHours(15), unalignedTo);
74+
}
75+
5176
[Fact]
5277
public void Hourly_NullCeiling_SaysUnknown_NotThatNoSpanWasServed()
5378
{

‎Darling/Darling.Tests/HourlyWindowEdgesTests.cs‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,30 @@ public void AlignedStart_HasNoLeadingSentence()
3333
Assert.DoesNotContain("partial hour", note, StringComparison.Ordinal);
3434
}
3535

36-
[Theory]
37-
[InlineData(20)]
38-
[InlineData(0)]
39-
public void End_CountsTheWholeHourPastAsOf(int minute)
36+
[Fact]
37+
public void UnalignedEnd_CountsTheWholeHourPastAsOf()
4038
{
41-
var end = Aligned.AddHours(4).AddMinutes(minute);
39+
var end = Aligned.AddHours(4).AddMinutes(20);
4240
var note = HourlyWindowEdges.Note(Aligned, Aligned, end, Aligned.AddHours(9));
4341
Assert.Contains("up to " + Aligned.AddHours(5).ToString("o"), note, StringComparison.Ordinal);
4442
}
4543

44+
/// <summary>The hourly reads stop BEFORE the window end (<c>bucket &lt; end</c>), so an end exactly on the
45+
/// hour no longer takes the hour that begins there: the served span ends at the window end and the note
46+
/// claims nothing past it. (This case used to be pinned as "counts the whole hour", when the bound was
47+
/// <c>bucket &lt;= end</c>.)</summary>
48+
[Fact]
49+
public void AlignedEnd_StopsAtTheEnd_AndCountsNothingPastAsOf()
50+
{
51+
var end = Aligned.AddHours(4);
52+
var note = HourlyWindowEdges.Note(Aligned, Aligned, end, Aligned.AddHours(9));
53+
Assert.DoesNotContain("included whole", note, StringComparison.Ordinal);
54+
Assert.DoesNotContain("counted past", note, StringComparison.Ordinal);
55+
56+
var (_, servedEnd) = HourlyWindowEdges.ServedSpan(Aligned, Aligned, end, Aligned.AddHours(9));
57+
Assert.Equal(end, servedEnd);
58+
}
59+
4660
[Fact]
4761
public void EndCutAtTheCeiling_SaysNothingAfterItWasRead()
4862
{

‎Darling/Darling.Tests/QueryStoreTrendRoutingTests.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ public void RollupTrendSql_ServesTheRollup_AndPartitionsAtTheBoundary(bool withD
184184
/* The partition seam: rollup buckets strictly BELOW $4 (load-bearing against a refresh landing
185185
between the probe and the read), raw points at or above it. */
186186
Assert.Contains("bucket >= $2", sql, StringComparison.Ordinal);
187-
Assert.Contains("bucket <= $3", sql, StringComparison.Ordinal);
187+
/* The window end is exclusive too: a bucket is stamped at its START, so `bucket <= $3` would take the
188+
whole hour that begins at an end falling on the hour. */
189+
Assert.Contains("bucket < $3", sql, StringComparison.Ordinal);
190+
Assert.DoesNotContain("bucket <= $3", sql, StringComparison.Ordinal);
188191
Assert.Contains("bucket < $4", sql, StringComparison.Ordinal);
189192
Assert.Contains("interval_start_time_utc >= $4", sql, StringComparison.Ordinal);
190193

0 commit comments

Comments
 (0)