Repository navigation
Deprecated Dashboard analysis perfmon reads divide by the measured interval (#3561) #3569
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
Merged
Merged
Changes from all commits
Commits
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,102 @@ | ||
| /* | ||
| * Performance Monitor Dashboard | ||
| * Copyright (c) 2026 Darling Data, LLC | ||
| * Licensed under the MIT License - see LICENSE file for details | ||
| */ | ||
|
|
||
| using PerformanceMonitorDashboard.Analysis; | ||
| using Xunit; | ||
|
|
||
| namespace PerformanceMonitorDashboard.Tests; | ||
|
|
||
| /// <summary> | ||
| /// #3527/#3561: cntr_value_delta spans one COLLECTION INTERVAL, not one second — read raw, the | ||
| /// perfmon analysis reads overstate by the cadence (60x at 60s, 300x at 5min). All three reads | ||
| /// must divide by the row's measured sample_interval_seconds and skip interval <= 0 rows | ||
| /// (no delta was knowable: first sighting, reset, gap) — together or the baseline comparison is | ||
| /// cross-unit. The Dashboard has no test database, so these pin the SQL text, the mirror of the | ||
| /// Darling twin's #3560 pins (PgFactCollector.PerfmonSql / PgAnomalyDetector.BatchRequestWindowSql). | ||
| /// </summary> | ||
| public class PerfmonPerSecondSqlTests | ||
| { | ||
| /* ---------------- read 1: the fact collector ---------------- */ | ||
|
|
||
| [Fact] | ||
| public void PerfmonFactSql_SelectsTheMeasuredInterval_AndFiltersUnknowableRows() | ||
| { | ||
| var sql = SqlServerFactCollector.PerfmonSql; | ||
|
|
||
| /* The interval rides both the CTE and the outer select so the C# division has its divisor. */ | ||
| Assert.Contains("sample_interval_seconds,", sql); | ||
| Assert.Contains("SELECT counter_name, cntr_value, cntr_value_delta, sample_interval_seconds", sql); | ||
|
|
||
| /* Interval <= 0 rows are filtered INSIDE the CTE, so rn = 1 lands on the newest row a rate | ||
| can honestly be derived from — a counter with only interval-0 rows emits no fact, never 0. */ | ||
| Assert.Contains("AND sample_interval_seconds > 0", sql); | ||
| var filterAt = sql.IndexOf("AND sample_interval_seconds > 0", System.StringComparison.Ordinal); | ||
| var rnFilterAt = sql.IndexOf("FROM latest WHERE rn = 1", System.StringComparison.Ordinal); | ||
| Assert.True(filterAt >= 0 && filterAt < rnFilterAt, "the interval filter must sit inside the latest CTE, before rn = 1"); | ||
| } | ||
|
|
||
| /* ---------------- read 2: the anomaly detector's window ---------------- */ | ||
|
|
||
| [Fact] | ||
| public void BatchRequestWindow_DividesByMeasuredInterval_AndSkipsUnknowableRows() | ||
| { | ||
| var sql = SqlServerAnomalyDetector.BatchRequestWindowSql; | ||
|
|
||
| Assert.Contains("AVG(cntr_value_delta * 1.0 / NULLIF(sample_interval_seconds, 0))", sql); | ||
| Assert.Contains("MAX(cntr_value_delta * 1.0 / NULLIF(sample_interval_seconds, 0))", sql); | ||
| Assert.Contains("sample_interval_seconds > 0", sql); | ||
|
|
||
| /* A raw AVG/MAX of the delta is exactly the #3527 defect — pin its absence. */ | ||
| Assert.DoesNotContain("AVG(cntr_value_delta)", sql); | ||
| Assert.DoesNotContain("MAX(cntr_value_delta)", sql); | ||
| } | ||
|
|
||
| /* ---------------- read 3: the baseline arm ---------------- */ | ||
|
|
||
| [Fact] | ||
| public void BatchRequestBaseline_PopulationIsPerSecond_RestartSignatureStaysRaw() | ||
| { | ||
| var sql = SqlServerBaselineProvider.GetBaselineQuery(SqlServerMetricNames.BatchRequests); | ||
|
|
||
| Assert.NotNull(sql); | ||
|
|
||
| /* The baseline population is the per-second rate, in the detector's window unit. */ | ||
| Assert.Contains("cntr_value_delta * 1.0 / NULLIF(sample_interval_seconds, 0) AS v", sql); | ||
| Assert.Contains("AVG(v) AS mean_val", sql); | ||
| Assert.Contains("STDEV(v) AS stddev_val", sql); | ||
| Assert.Contains("AND sample_interval_seconds > 0", sql); | ||
|
|
||
| /* The restart signature stays on the RAW delta: its > 1000 bar predates the division and | ||
| marks a counter reset regardless of cadence. */ | ||
| Assert.Contains("LAG(cntr_value_delta) OVER (ORDER BY collection_time) AS prev_value", sql); | ||
| Assert.Contains("WHERE NOT (cntr_value_delta = 0 AND ISNULL(prev_value, 0) > 1000)", sql); | ||
|
|
||
| /* A raw AVG/STDEV of the delta is exactly the #3527 defect — pin its absence. */ | ||
| Assert.DoesNotContain("AVG(cntr_value_delta)", sql); | ||
| Assert.DoesNotContain("STDEV(cntr_value_delta)", sql); | ||
| } | ||
|
|
||
| /* ---------------- the consistency constraint ---------------- */ | ||
|
|
||
| /// <summary> | ||
| /// The whole fix is the three reads agreeing: the window statistic, the baseline population, | ||
| /// and the fact value all divide by the measured interval, or the z-score compares across | ||
| /// units. The fact collector's division happens in C# (delta / sample_interval_seconds after | ||
| /// the SQL delivers the divisor), so its SQL is pinned for the divisor column and the | ||
| /// interval filter; the two aggregate reads divide in the SQL itself. | ||
| /// </summary> | ||
| [Fact] | ||
| public void AllThreeReads_FilterUnknowableRows() | ||
| { | ||
| var factSql = SqlServerFactCollector.PerfmonSql; | ||
| var windowSql = SqlServerAnomalyDetector.BatchRequestWindowSql; | ||
| var baselineSql = SqlServerBaselineProvider.GetBaselineQuery(SqlServerMetricNames.BatchRequests); | ||
|
|
||
| Assert.Contains("sample_interval_seconds > 0", factSql); | ||
| Assert.Contains("sample_interval_seconds > 0", windowSql); | ||
| Assert.Contains("sample_interval_seconds > 0", baselineSql); | ||
| } | ||
| } |
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.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking observation, out of scope for this PR:
collect.perfmon_stats.cntr_value_per_second(the computed column defined ininstall/02_create_tables.sql,cntr_value_delta / NULLIF(sample_interval_seconds, 0)) does integer division — both operands are integral types, so it truncates fractional per-second rates (e.g. 150/60 → 2, not 2.5). This PR correctly avoids it by using* 1.0 /for float division in all three reads it touches, butDatabaseService.ResourceMetrics.Perfmon.cs(lines 48 and 126) still selects that computed column directly for the perfmon chart UI, so that read path still carries a truncated-rate defect distinct from the #3527/#3561 cadence bug this PR fixes. Worth a follow-up issue if not already tracked — not asking for it here since it's an unrelated file/read path.