Skip to content

Plan viewer: a shared MetricFormatter for costs and durations - #4592

Merged
erikdarlingdata merged 1 commit into
devfrom
viewer/4577-metric-formatter
Sep 28, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
viewer/4577-metric-formatter

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4577, Part of #4511

Why

erikdarlingdata/PerformanceStudio@caa57ce (dev at 85492a1) added MetricFormatter and routed every raw numeric cost/duration display through it. The plan viewer is shared between the Darling Viewer and Lite, and until this PR it still formatted optimizer costs with a raw :F6, so the properties panel and node tooltips showed strings like 16.765900 or 0.000000 instead of a trimmed, thousands-separated number.

What changes

  • New PerformanceMonitor.PlanAnalysis/MetricFormatter.cs, ported near-verbatim from PerformanceStudio's PlanViewer.Core/Output/MetricFormatter.cs:
    • FormatCost: at most four decimal places, trailing zeros stripped, thousands separators, exact zero shown as "0", a non-zero cost too small for four decimals shown as "<0.0001" rather than rounding away to "0", never scientific notation.
    • FormatDuration(long): the existing three-rung ladder (ms under a second, seconds with one decimal under a minute, minutes+seconds beyond that).
    • FormatDuration(double): two more rungs below one millisecond (e.g. "0.4ms", "<0.01ms"), delegating to the long overload at and above 1ms.
  • PlanViewerControl.Properties.cs (Operator/Subtree/I/O/CPU Cost rows) and PlanViewerControl.Tooltips.cs (the same four cost rows) now call MetricFormatter.FormatCost instead of :F6.
  • PlanViewerControl.xaml.cs's StatementRow.FormatDuration(long) private helper is removed; CpuDisplay/ElapsedDisplay/UdfDisplay now call MetricFormatter.FormatDuration directly. CpuMs/ElapsedMs/UdfMs are long (showplan's CpuTime/ElapsedTime/UdfElapsedTime XML attributes parse to long in ShowPlanParser), so the sub-millisecond double overload has no call site in the statements grid; it's ported for the Query Store path PerformanceStudio's issue names, which PM doesn't yet have wired to a display.
  • CostDisplay (EstCost:F2, the grid's estimated-subtree-cost column) is unchanged: PerformanceStudio's routing didn't touch its own equivalent statements-grid cost column shape (F2, not F6), and it isn't one of the four Operator/Subtree/I/O/CPU cost rows this issue calls out.
  • MemoryFractionInput/MemoryFractionOutput (:F4) are a different metric family PerformanceStudio leaves alone; unchanged.

Lite: gets this too (shared control) — PlanViewerControl is hosted by both the Darling Viewer and Lite.

Not ported: PerformanceStudio's comparison-report and web-viewer routing sites have no PM equivalent (no comparison report, no Blazor/web viewer).

Test plan

New Darling/Darling.Tests/Viewer4577Tests.cs, ported from PerformanceStudio's MetricFormatterTests.cs (all cases, all InvariantCulture-pinned), plus a census test (NoRawCostFormatStringSurvivesInTheUi) that walks PerformanceMonitor.Ui/*.cs and fails if any file still contains :F6.

  • Build (macOS, EnableWindowsTargeting=true, Release): Darling.Tests, PerformanceMonitor.Ui, Lite/PerformanceMonitorLite.csproj, Lite.Tests, Darling/PerformanceMonitor.Darling.Viewer — all 0 warnings / 0 errors.
  • RED on the pre-fix code (git worktree add --detach <pre-fix sha>): copying Viewer4577Tests.cs in without MetricFormatter.cs is a compile failure (CS0103: The name 'MetricFormatter' does not exist) — expected, since the class is new. A second, runtime-only copy of just the census assertion (no MetricFormatter reference) run against the pre-fix PlanViewerControl.Tooltips.cs/Properties.cs fails at runtime: Raw ":F6" cost formatting left in: .../PlanViewerControl.Tooltips.cs, .../PlanViewerControl.Properties.cs.
  • Mutation on the fixed code: changed FormatCost's cost == 0 branch to return "MUTATED"; FormatCost_ShapesTheNumber(cost: 0, expected: "0") failed as expected; reverted; rebuilt clean.
  • In-process run (Darling.Tests.dll, Microsoft.WindowsDesktop.App stripped from runtimeconfig): Viewer4577Tests (46 cases) plus DocCommentHygieneTests, ShowPlanParserCondAndMultiplePlanTests, ActualPlanRequestTests, ActualPlanDispatchTests, ActualPlanResultParseTests, QueryModificationDetectorTests, ActualPlanCaptureLoopTests, ActualPlanGatingTests, ReproScriptBuilderHardeningTests, DarlingAnalysisPipelineTests, DarlingMcpPlanToolsSurfaceAndSqlTests, McpPlanAnalysisEnvelopeTests, SerialLoopStoreSizeSourceTests, TsqlConventionGuardTests:
    Total: 276, Errors: 0, Failed: 0, Skipped: 1, Not Run: 0 (DarlingMcpPlanToolsLivePostgresTests skips — no local Postgres rig).
  • Darling.Tests/Lite.Tests target net10.0-windows: they build on macOS but Lite.Tests cannot run here; CI decides Lite.Tests. No Lite.Tests file was touched by this change, so nothing new needs a Windows-only read.
  • Screenshot plan (pure layout, for a human to check in Lite and in the Darling Viewer): open a plan with a non-trivial optimizer cost (e.g. an operator cost like 16.7659 or a tiny cost like 0.00002), check the Properties panel's Operator/Subtree/I/O/CPU Cost rows read as trimmed numbers with thousands separators (not 16.765900), and hover a node to confirm the tooltip's Cost/Subtree Cost/I/O Cost/CPU Cost rows match the same formatting.

CHANGELOG

SECTION: Changed
ENTRY: - Plan viewer costs are formatted as in PerformanceStudio ([#4592]) - Costs in the properties panel and node tooltips show a trimmed, thousands-separated number instead of six raw decimal places. The sub-millisecond duration format is ported too, but the viewer's statement times are whole milliseconds today, so nothing shows it yet.

REF: [#4592]: #4592

Ports erikdarlingdata/PerformanceStudio@caa57ce's MetricFormatter.cs and its
tests to PerformanceMonitor.PlanAnalysis, and routes the properties panel's
Estimated Costs and the node tooltips' cost rows through it, replacing raw
":F6" formatting that showed costs like "16.765900" or "0.000000".

FormatCost caps a cost at four decimals, strips trailing zeros, adds
thousands separators, shows an exact zero as "0", and shows a non-zero cost
too small for four decimals as "<0.0001" rather than rounding it away to
"0". It never falls back to scientific notation.

FormatDuration keeps the existing three-rung ladder (ms under a second,
seconds with one decimal under a minute, minutes+seconds beyond that) and
adds a double overload with two rungs below a millisecond, so a
sub-millisecond duration reads as e.g. "0.4ms" instead of rounding to
"0ms". The statements grid's private FormatDuration(long) now delegates to
the shared helper instead of keeping its own copy of the same ladder.

MemoryFractionInput/Output keep their own ":F4" formatting; that's a
different metric family this change leaves alone.

A census test asserts no ":F6" cost formatting is left anywhere in
PerformanceMonitor.Ui.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 28, 2026 15:35
@erikdarlingdata
erikdarlingdata merged commit 52a3aa1 into dev Sep 28, 2026
17 of 20 checks passed
@erikdarlingdata
erikdarlingdata deleted the viewer/4577-metric-formatter branch September 28, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant