Repository navigation
Fixes #2855 - #2893
Fixes #2855#2893
Conversation
) The Azure SQL DB per-database loop measured one blended per-database total, so a slow database could not be attributed even in principle - the last collection path still reporting a single number after #2164/ #2312 (enumerated) and #2851 (server-scoped). Its split is a THIRD decomposition, not a copy of either: this branch opens a connection per database, so OpenDatabaseConnectionAsync is a phase in its own right. The stamps take a PerDatabase*Ms prefix rather than reusing PerItem*Ms, because the enumerated log site keys on its own flag and prints an item name - sharing the fields would make an Azure run print as an enumerated one, the exact collision ServerScopeOpenMs was given its own prefix to avoid. So ServerScopePhaseSplitTests' derivation is widened in the same change and MinimumPhaseStamps goes 12 -> 16 (the exact count; 12 was already one behind the true 13, since #2864's ServerScopeLastReadMs was covered automatically by the pattern). Every stamp and the gating flag are set from a finally, so a login that times out or a budget that fires mid-handshake still reports the phase it died in. The log line is gated on PerDatabasePhasesMeasured through a single shared expression, never on a figure being non-zero: a pooled connect can legitimately measure 0ms, and that is the database whose open: and drain: are worth reading. The pin now also covers the measured FLAGS' handler reachability, which nothing did before - the *Ms derivation selects on long, so no bool could ever appear in it, and a flag assigned after the await leaves a faulting phase declaring itself unmeasured while the arithmetic pin stays green. Emit-and-log only. A per-database split is N:1 against collection_log where the server-scoped split is 1:1, and that cardinality question is already open as #2860, so nothing is persisted and the schema stays at 109. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two self-review corrections, no behaviour change. other: on this path also absorbs the TEARDOWN of the reader, command and connection - the host reads dbSqlMs after the using block closes, so one close per database sits inside the parent stopwatch. Named, because "a large other: is itself the finding" is only actionable if the reader knows what is in it. The watermark read and BuildQuery are named as NOT in it, since they run before the stopwatch starts. The per-database fixtures borrowed 1,451 / 247 / 3,900 / 4,644 from the measurements in this file's header, which belong to the store probe and to procedure_stats. Nothing has measured this branch against a live Azure SQL DB target, so a fixture echoing a real figure from another path reads as if something had. Replaced with obviously-invented values and a comment saying why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The log site sits on the success path, so a database whose connect times out stamps its phases and sets its flag - both from the finally - and then reaches a catch arm that prints no split. Same standing as the enumerated path's own #2854 note, and worth saying outright in both the runner and the pin's header rather than leaving a reader to infer that "stamped from finally" means "reported on the fault path". Not fixed here because the parent this line decomposes is out of scope in the catch: sqlSlice is declared inside the try, and hoisting it above the watermark read and BuildQuery would silently widen dbSqlMs, which feeds sqlMs, the fan-out rollup and collection_log's sql_duration_ms. The remaining choices are a second stopwatch or a second line shape, and neither belongs in a change scoped to the split itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
One deferral, filed rather than dropped: #2896. Every stamp and the flag are set from a Not fixed here because the line decomposes Stated in the code too, at the log site and in the pin's header, rather than left for a reader to infer that "stamped from |
ReviewWent through the diff ( Correctness
Lite/Darling parity
Security / perf
No blocking issues found. Nice adversarial test coverage (all 7 regressions in the PR description check out against the shipped code). |
|
Reviewed for correctness, Lite/Darling parity, security, and performance. Correctness: Traced the new connect/open/drain instrumentation in
Lite/Darling parity: Security/Perf: No new external input handling, no schema/query changes, no persistence — only structured logging (parameterized, no string interpolation into the message template) and a few extra No issues found. This is a well-scoped, well-tested change. |
|
Reviewed the diff (CHANGELOG.md, Correctness: The connect/open/drain try/finally structure is sound — each phase stamp is written from its own
Lite/Darling parity: No parity gap. This entire phase-split logging mechanism ( Schema/persistence: Correctly emit-and-log only — no Style: T-SQL style section doesn't apply (no No correctness, security, or performance issues found. Nice test coverage — the seven red-first regression cases listed in the PR description line up with what's actually pinned in |
#2893 extended ServerScopePhaseSplitTests in the same file this change converts: a third PerDatabase*Ms prefix, MinimumPhaseStamps 12 -> 16, and a new MeasuredFlagSetters set of bool *PhasesMeasured flags scanned by the same IL walk. Took that lane's file wholesale and replaced only ScanServiceAssembly, preserving its widened tracked set (PhaseSetters + MeasuredFlagSetters + ControlSetter) rather than the narrower one this branch had. Their derived tracked set is exactly why the MethodSpec gap mattered here: a derivation picks up a stamp the day it appears, so a generic one would have been derived and then read as never called. Noted at the call site. All 14 of their pins pass on the shared scanner, including the two IL ones.
What
The Azure SQL DB per-database branch in
DarlingCollectorRunner.RunAsyncemitted no phase split at all — not a mis-stamped one, none — so a slow database could not be attributed even in principle. It was the last collection path still reporting one blended number, after #2164/#2312 (enumerated) and #2851 (server-scoped). Distinct from #2854, which moved stamps that already existed.Each database now logs, on its own line:
Why the shape is a THIRD decomposition
This branch opens a connection per database, which neither other path does — the enumerated path reuses one target connection across items, the server-scoped path opens one for the whole server. So
OpenDatabaseConnectionAsyncis a phase in its own right: on Azure SQL DB a fresh login per database per cycle, and the same branch serves the per-database PostgreSQL collectors, where one connection per database is unavoidable because a PostgreSQL session is bound to one database for its lifetime.drain:is measured, not inferred, soother:is a real residual (command construction, the trailing probe-failure rowset) — the #2851 departure, for the same reason.The line is not gated on
batch.Count > 0, unlike the enumerated path's quiet-on-zero rule. That rule guards a ROW-COUNT line, whose payload is the row count. This is a PHASE line, and the connect is paid on a quiet database exactly as on a busy one; the server-scoped phase line is the closer sibling and prints for every measured run.Naming:
PerDatabase*Ms, and the pin widened in the same changeReusing
PerItem*Mswould have made an Azure run print as an enumerated one — the enumerated log site keys on its own flag and prints an item name. That is the exact collisionServerScopeOpenMs's doc comment records as the reason the server-scoped path was given its own prefix, so the precedent decided it.The price of the honest name is that
ServerScopePhaseSplitTests' derivation had to be widened here, since a stamp outside the pattern is a stamp nothing guards — worse than no stamp, that file's whole claim being that a new one is covered the day it appears.MinimumPhaseStampsgoes 12 → 16, the exact count rather than a round number. 12 was already one behind the true 13: #2864'sServerScopeLastReadMsmatched the pattern the day it landed and was covered automatically — exactly as designed — but left room for a deletion to go unnoticed.The flag, and a gap it exposed
Every stamp and the gating flag are set from a
finally, so a login that times out or a per-database budget that fires mid-handshake still reports the phase it died in. The log line gates oncontext.PerDatabasePhasesFrom(dbSqlMs)— one shared expression, so "no split" and "no line" are one decision — never on a figure being non-zero: a pooled connect can legitimately measure0ms, and that is precisely the database whoseopen:anddrain:are worth reading.While wiring that: the measured flags' handler reachability was guarded by nothing. The
*Msderivation selects onlong, so no bool could ever appear in it. A flag assigned after the await leaves a faulting phase declaring itself unmeasured and printing nothing, while the number is stamped correctly and the arithmetic pin stays green.MeasuredFlags_AreReachableFromExceptionHandlers_...now derives the*PhasesMeasuredsetters and scans them through the same IL walk — which also covers the pre-existingPerItemPhasesMeasured(already correct, so it passes).Verification
Windows CI is the arbiter for
Darling.Tests. Locally (macOS) I compiled the real shippedServerScopePhaseSplitTests.csinto a throwawaynet10.0xunit.v3 host — it has no Windows dependency — and ran it against the built assemblies: 14 passed, 0 failed (7 pre-existing + 7 new). A separate reflection/IL harness over the builtPerformanceMonitor.Collectors.dllandPerformanceMonitor.Darling.Service.dllconfirms 16 derived stamps and that the three new members match the pin's selector (long, settable,*Ms,PerDatabase*), with the harness asserting its loaded DLLs are byte-identical to the projects' own bin output.Seven regressions proven red first, each on a different assertion:
Only 15 phase stamps ... expected at least 16Only 16 ... expected at least 17Only 13 ... expected at least 16finallyset_PerDatabaseConnectMs is never invoked from inside an exception handler (2 call site(s), all on success paths)finallyset_PerDatabasePhasesMeasured is never set from inside an exception handler (2 call site(s), all on success paths)connect > 0Assert.Null() Failure ... has a value(prints a stale split) andAssert.NotNull() Failure ... does not have a value(drops a measured zero)Assert.DoesNotContain() Failure: Sub-string foundNot verified
Darling.Tests/Lite.Testsas suites were not run locally (net10.0-windows). Only the one pin file above was executed, in a net10.0 host.Scope
Visibility, not a speedup: no query, schedule, collector or connection behaviour changes. Emit-and-log only — a per-database split is N:1 against
collection_logwhere the server-scoped split #2859 persists is 1:1, and #2860 already holds that cardinality question open for the per-database fetch split. No column, no rung; schema stays at 109.Fixes #2855
🤖 Generated with Claude Code