Repository navigation
Name the queries behind PARAMETER_SENSITIVITY and QUERY_SPILLS (#6640) - #6641
Conversation
Both facts now set ObjectName and Ranked (worst first, at most three). PARAMETER_SENSITIVITY names database/query_hash/query_plan_hash. QUERY_SPILLS names database/query_hash by spilled pages. The card text lists them, and get_analysis_facts carries object_name for these two facts. Same change on Lite.
| Value = totalSpills, | ||
| ServerId = context.ServerId, | ||
| ObjectName = spillRanked.Count > 0 ? spillRanked[0].ObjectName : null, | ||
| Ranked = spillRanked, |
There was a problem hiding this comment.
QUERY_SPILLS breaks the Ranked invariant: entry [0]'s value is not the fact's value.
RankedObject's doc states the invariant the whole seam rests on (PerformanceMonitor.Analysis/AnalysisModels.cs:11-14):
The invariant. Entry
[0]IS the fact's own subject —Fact.ObjectNameandFact.Value— so a card has ONE truth about what it is about and the advice never has to decide between two spellings of the worst object.
FactRankedTests.AssertInvariant enforces exactly that, by value (Darling/Darling.Tests/FactRankedTests.cs:51-52):
Assert.Equal(fact.ObjectName, fact.Ranked[0].ObjectName);
Assert.Equal(fact.Value, fact.Ranked[0].Value, precision: 9);Here Value = totalSpills (the window's total across every spilling query) while spillRanked[0].Value is the top query's own spilled pages. The name half of the invariant holds (line 132); the value half does not. Darling has the same shape at Darling/PerformanceMonitor.Darling.Analysis/PgFactCollector.QueryPerf.cs:133-136, and the PR's own tests pin the disagreement rather than catching it — Lite.Tests/FactsNameQueriesTests.cs:471 asserts total_spills == 1100 and :477 asserts ranked values 500, 300, 200. Dropping FactRankedTests.AssertInvariant(fact) into either new test fails today.
This is not only a doc mismatch: both SKUs' get_analysis_facts descriptions promise it to agents, verbatim —
entry [0] being the fact's own subject (same object_name and value)
Failure scenario: a window with 1,100 spilled pages across four queries emits value: 1100 on the card and ranked[0].value: 500. An agent (or analyze_server's root_fact, where the payload is { key, value } + ranked and there is no object_name at all — Lite/Mcp/McpAnalysisTools.cs:217-220) reads the two as the same quantity and reports 1,100 pages against Db/0xSB, or concludes the ranked list disagrees with its own card. PARAMETER_SENSITIVITY is fine here precisely because its Value is the worst offender's ratio.
Either keep entry [0] faithful to Value (e.g. rank on a quantity the fact's own Value states, and carry per-query pages in Figures), or make the exception explicit — amend RankedObject's invariant, FactRankedTests.AssertInvariant, and the sentence in both tool descriptions — rather than leaving three places asserting something the shipped fact no longer does.
| var result = filtered | ||
| .OrderByDescending(f => f.Severity) | ||
| .Select(f => FactRanked.Attach(new | ||
| .Select(f => FactRanked.Attach(FactRanked.AttachSubject(new |
There was a problem hiding this comment.
This wiring red-lines an existing test on both SKUs.
FactRankedTests.BothToolFiles_AttachTheRankedBlockOncePerTool_AndNeverSpellItAsAProperty reads both tool files as source and pins the two literals this hunk rewrote (Darling/Darling.Tests/FactRankedTests.cs:152-153):
Assert.Equal(1, CountOf(facts, "FactRanked.Attach(new"));
Assert.Contains("}, f.Ranked, McpHelpers.JsonOptions))", facts, StringComparison.Ordinal);After the change the body reads FactRanked.Attach(FactRanked.AttachSubject(new (so "FactRanked.Attach(new" counts 0, not 1) and closes with }, f, McpHelpers.JsonOptions), f.Ranked, McpHelpers.JsonOptions)) (so the second needle, which requires } immediately before , f.Ranked, is absent). The loop runs over both SKUs, so both assertions fail twice — once for Lite/Mcp/McpAnalysisTools.cs:363,390 and once for Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpTools.cs:397,424. .github/ci-quarantine.txt is empty, so nothing exempts it.
It is a source-reading pin, so per the #5833 ruling it is not coverage and should not be answered with another one — but it is in the tree and it will fail the build. It needs updating (or deleting under that ruling) in this PR, and the PR body's "The full suites also wait for CI" does not mention it.
|
|
||
| var node = JsonSerializer.SerializeToNode(payload, options)?.AsObject() | ||
| ?? throw new InvalidOperationException("the fact payload did not serialize to a JSON object"); | ||
| node["object_name"] = fact.ObjectName; |
There was a problem hiding this comment.
A new payload field no tool description mentions — and the field's own doc now says the opposite of what the code does.
object_name reaches the wire here, but neither SKU's get_analysis_facts description was touched. The rule this repo already wrote for this exact seam (Darling/Darling.Tests/FactRankedTests.cs:135-137):
Both tools' descriptions state the field and its two-or-more rule, because a payload field an agent is not told about is a field it will not read.
The single-query case is the whole reason AttachSubject exists (ranked rides only at 2+), and that is precisely the case an agent will not look for, because Lite/Mcp/McpAnalysisTools.cs:278 and Darling/.../DarlingMcpTools.cs:312 still describe the fact entry as source / key / value / base_severity / severity / metadata / amplifiers plus ranked.
Two more stale statements in the same seam that this diff falsifies:
PerformanceMonitor.Analysis/AnalysisModels.cs:71-73—Fact.Ranked's doc: "Empty for the overwhelming majority of facts … and no SQL Server collector sets this at all." Both new call sites are SQL Server collectors.analyze_server's description enumerates who ranks — "A finding whose root fact ranked several objects (the autovacuum-disabled and vacuum-backlog cards, the bloat and growth trends) carriesroot_fact.ranked" — andQUERY_SPILLS/PARAMETER_SENSITIVITYnow do too, on both SKUs, viaRootFactRanked = rootFact?.Ranked ?? [](InferenceEngine.cs:383).
Worth deciding at the same time whether analyze_server's root_fact should carry the subject too: it is { key, value } + attached ranked (Lite/Mcp/McpAnalysisTools.cs:217-220), so on the one-spilling-query finding — the case this PR is about — the query is named only in the English advice sentence, which is the thing ranked was introduced to stop agents from parsing.
| Key = "QUERY_SPILLS", | ||
| Value = totalSpills, | ||
| ServerId = context.ServerId, | ||
| ObjectName = spillRanked.Count > 0 ? spillRanked[0].ObjectName : null, |
There was a problem hiding this comment.
These two keys now name something, so they owe FactIdentity a row — otherwise the name vanishes whenever the fact is consumed as a hop.
FactIdentity is the roster of keys that name a thing, and its doc states the membership rule (PerformanceMonitor.Analysis/FactIdentity.cs:43-45): "membership is an explicit, pinned list per engine, and a key joins it by a one-row edit here that says what it names." SqlServerKeys (:76-82) is still BLOCKING_CHAIN, RUNNING_JOBS, ANOMALY_OBJECT_GROWTH, ANOMALY_OBJECT_CONTENTION.
Both new keys are edge destinations, so the greedy traversal consumes them under higher-severity roots routinely:
RESOURCE_SEMAPHORE → QUERY_SPILLS(RelationshipGraph.cs:198)MEMORY_GRANT_PENDING → QUERY_SPILLS(:208)TEMPDB_USAGE → QUERY_SPILLS(:403)CPU_SPIKE → PARAMETER_SENSITIVITY(:168),MEMORY_GRANT_PENDING → PARAMETER_SENSITIVITY(:223)
Failure scenario: RESOURCE_SEMAPHORE at 1.25 consumes QUERY_SPILLS at 0.9. The only card the operator and the e-mail get is the root's, FactIdentity.Describe returns null for QUERY_SPILLS, so no Behind it: sentence is composed — and the spilling query this PR just taught the collector to identify is named nowhere on the surface that is actually read. That is the #3691 defect shape FactIdentity exists to close, now reachable on two more keys; before this PR there was no name to lose.
Two rows ("QUERY_SPILLS", "PARAMETER_SENSITIVITY") plus their Describe arms — e.g. query hash `…` the way SqlServerBadActorPrefix already renders one (:140) — would close it. If it is deliberately out of scope for #6640, a sentence in the PR body saying so would do.
| /// <summary>#6640: the queries behind QUERY_SPILLS, most spilled pages first. Same window, filter and (database, query_hash) | ||
| /// identity as the aggregate, so the names are the queries its spilling_query_count counts. Run only when spills exist.</summary> | ||
| private static readonly string QuerySpillsRankedSql = @" |
There was a problem hiding this comment.
Non-blocking: the new field landed between CollectQueryStatsFactsAsync's doc comment (lines 16-19) and the method, so the field now carries two <summary> elements — "Collects query-level aggregate facts from query_stats. Focuses on spills…" plus the #6640 one — and the method is left undocumented. Darling got this right (PgFactCollector.QueryPerf.cs:65-86: the constant sits above the method's own summary). Moving the field above line 16 restores both.
Also minor parity: Darling declares public static readonly string QuerySpillsRankedSql while Lite's is private. Nothing references the Darling one (the new live test builds its facts through PgFactCollector), so private/internal on both would match.
| server_time_zone_id, | ||
| database_name, | ||
| query_hash, | ||
| query_plan_hash |
There was a problem hiding this comment.
Non-blocking, but it is new as of this hunk: these three columns turn the parameter-sensitivity read's row order into published names, and that statement's ORDER BY worker_ratio DESC (line 269, and PgFactCollector.QueryPerf.cs:253) has no tiebreak. The new spills read deliberately added one — ORDER BY spills DESC, v.database_name, v.query_hash (line 34) — so the two halves of the same PR disagree.
Two plans at the same max/min worker ratio (10.0 is a common floor-hugging value, and the cap keeps only three) can swap places between passes, so the card's "Worst plans (database/query_hash/query_plan_hash): …" sentence and ranked[0] / object_name name a different plan each run, on identical data — and worstRatio, worstMinWorker etc. are read off whichever row landed first. Appending , database_name, query_hash, query_plan_hash to both SKUs' ORDER BY makes the pick stable and the two reads consistent with each other.
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
Collectors, advice sentence and payload helper do what the body says, and Lite/Darling behaviour parity is real. Not accounted for: QUERY_SPILLS now contradicts the Ranked invariant three places still assert, the tool wiring red-lines an existing pin on both SKUs, and object_name is documented nowhere an agent reads. Details in the six inline comments.
Claims
- VERIFIED — PARAMETER_SENSITIVITY sets
ObjectName/Ranked; read returnsdatabase_name,query_hash,query_plan_hash; name isdatabase/query_hash/query_plan_hash—DuckDbFactCollector.QueryPerf.cs:259-261,:317-321,:343-344;PgFactCollector.QueryPerf.cs:243-245,:308-312,:336-337. - VERIFIED —
Valueis the worker ratio;Figurescarriesmin_worker_us,max_worker_us,grant_ratio,spill_divergence—DuckDbFactCollector.QueryPerf.cs:323-330(ordinal 2 is the columnworstRatioreads at:311);Lite.Tests/FactsNameQueriesTests.cs:453-455. - VERIFIED — existing Metadata keys unchanged —
:345-354and:134-140untouched; every hunk additive. - VERIFIED — second statement, same connection, ranks (database, query_hash) by spilled pages, top three, only when the aggregate found spills —
DuckDbFactCollector.QueryPerf.cs:106-122,PgFactCollector.QueryPerf.cs:110-125,LIMIT FactRanked.MaxObjects(3,AnalysisModels.cs:44). - VERIFIED — aggregate figures and thresholds untouched —
QueryStatsSqlunchanged; scorer armsFactScorer.cs:510,:515read the same values. - VERIFIED — card text lists the named queries for both facts —
FactAdvice.cs:1640,:1695viaNamedQueriesSentence(:1658-1666);FactsNameQueriesAdviceTestsasserts both wordings. - VERIFIED —
get_analysis_factsnever showedobject_name;AttachSubjectadds it for these two keys only —FactRankedPayload.cs:77-95; old entry shapeMcpAnalysisTools.cs:365-389. - VERIFIED — every other fact keeps its bytes —
FactRankedPayload.cs:88-89returns the caller's reference;FactsNameQueriesAdviceTests.cs:71-74. - VERIFIED — both MCP tools call it —
McpAnalysisTools.cs:363+:390,DarlingMcpTools.cs:397+:424. - VERIFIED — Lite has the same change, no migration rung, facts ephemeral — Lite hunks mirror Darling's; no store/ladder/
StorageVersiontouched;AnalysisModels.cs:75-78. - UNVERIFIED — 158 ms Postgres / 3-5 ms DuckDB vs 285-317 ms aggregate — live measurement over a 1M-row seed.
- UNVERIFIED — two production stores, 37.6/25.4 ms and 36.8/27.0 ms — those stores are unreachable here.
- UNVERIFIED — Darling live tests "passed in a scratch harness" — the class
Assert.SkipWhens withoutDARLING_TEST_PG(FactsNameQueriesLiveTests.cs:170), so neither this tree nor CI runs it. - UNVERIFIED — Lite tests "build locally" —
Lite.Testsis Windows-desktop; no build here. - UNVERIFIED — the revert PR #6643 fails each new test — needs that run.
No claim is refuted by the tree; the problems below are things the body does not claim either way.
Riskiest lines
Lite/Analysis/DuckDbFactCollector.QueryPerf.cs:133(twinPgFactCollector.QueryPerf.cs:136) —Ranked[0].Valueis one query's pages whileValueis the window total, againstAnalysisModels.cs:11-14,FactRankedTests.cs:52and both tool descriptions ("same object_name and value") — UNCOVERED:AssertInvariantis never called on these facts andLite.Tests/FactsNameQueriesTests.cs:471-477pins the mismatch (1100 vs 500/300/200) as expected.Lite/Mcp/McpAnalysisTools.cs:363(twinDarlingMcpTools.cs:397) — the nestedAttach(AttachSubject(...))is the only thing puttingobject_nameon the wire; a swapped nesting compiles and drops a block — UNCOVERED: the new test exercisesAttachSubjectdirectly, not the tool payload.PgFactCollector.QueryPerf.cs:306-321— ordinals 8/9/10 against the columns appended at:243-245, plusreader.CloseAsync()at:112before a second command on one pooled connection — UNCOVERED in CI (live-only, skipped withoutDARLING_TEST_PG); the Lite twin covers Lite's ordinals and its close at:108.
Traps near this diff
- "Ruling (#5833): tests are load-bearing … never answer a trap with a test that reads source text or asserts how code is spelled" — applies: the tool wiring is governed only by a source pin (
FactRankedTests.cs:152-153) that this change breaks on both SKUs; update or retire it, do not add another spelling pin. - "Lite and Darling ship in parity: a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit." — does not apply to behaviour (every site changed on both SKUs:
DuckDbFactCollector.QueryPerf.cs:22-35/:103-133/:255-261/:306-344vsPgFactCollector.QueryPerf.cs:68-81/:107-136/:243-245/:306-337). It does bite the descriptions: neither SKU documentsobject_name. Cosmetic only: Lite's constantprivatevs Darling'spublic, and Lite's doc comment on the wrong member. - "
FactAdvice.PopulateStoryTextruns BEFOREClusterIntoIncidents/StampClusters/Reconcile, so a sentence added after folding must re-read the frozenStoryTextblob (#3709)" — does not apply: appended insideComposeQuerySpills/ComposeParameterSensitivity(FactAdvice.cs:1640,:1695), so it is in the blob from the start. - "A count taken off the page and named as the population's … belongs on the statement above the
LIMIT(#3679)" — does not apply:spilling_query_countis stillCOUNT(DISTINCT …)above any limit (:79-80, read at:138); the three names are a cap beside it, as isoffender_count(:347). - "
ORDER BY 3 + 4 DESCis not an ordinal sum … sort by named columns (#3613)" — does not apply: the new read sorts named columns with a full tiebreak (:34,PgFactCollector.QueryPerf.cs:80). The adjacent hazard —ORDER BY worker_ratio DESCwith no tiebreak now deciding published names — is inline on:261, non-blocking. - "Every MCP filter is part of the query, not a post-filter on the page (#3641)" — does not apply: the
ranked.Count < MaxObjectscut (:315) must follow the exact compiled-before-the-window test #4821 moved out of SQL (:294-299); the spills read, with no such test, does cut in SQL. - "A fixture testing a shape no live run produces (#3709)" — does not apply: four sensitive plans and six spill snapshots in a four-hour window are real shapes, and both tests drive real collector code over planted rows.
Divergence (blind read vs PR body)
- The body calls the
QUERY_SPILLSentry's value "the spilled pages" without noting it is the firstRankedlist whose entry [0] does not restate the fact'sValue.RankedObject's invariant (AnalysisModels.cs:11-14),FactRankedTests.AssertInvariant(:51-52) and both tool descriptions still assert the old rule.PARAMETER_SENSITIVITYhonours it;QUERY_SPILLSdoes not, and the PR's own tests pin the disagreement. Either the fact or the invariant has to move. - "Both MCP tools call it" is true, and it breaks
FactRankedTests.BothToolFiles_AttachTheRankedBlockOncePerTool_AndNeverSpellItAsAPropertyon both SKUs:CountOf(facts, "FactRanked.Attach(new")is 0 where 1 is expected, and"}, f.Ranked, McpHelpers.JsonOptions))"is gone..github/ci-quarantine.txtis empty; "the full suites also wait for CI" is all the body says about existing tests. - No description or doc was updated for a field whose purpose is the single-query case:
object_nameis undocumented in bothget_analysis_factsdescriptions,Fact.Ranked's doc still reads "no SQL Server collector sets this at all" (AnalysisModels.cs:73), andanalyze_server's description still names only the four PostgreSQL families, while these two ride there viaInferenceEngine.cs:383. Related:root_factis{ key, value }+ranked(McpAnalysisTools.cs:217-220), so a one-spilling-query finding names the query only in prose.
Separately, FactIdentity's roster was not extended (inline on PgFactCollector.QueryPerf.cs:135): both keys are edge destinations (RelationshipGraph.cs:198, :208, :403, :168, :223), so a consumed hop's query name reaches no surface.
Reviewer checklist
- Build and run
Darling.Tests.FactRankedTeststo confirm the two source-pin assertions fail as read, and decide whether that pin is updated or retired under #5833. - Run
FactsNameQueriesLiveTestswithDARLING_TEST_PGagainst a real Postgres 16 store: Darling's ranked read, its mid-method reader close plus second command on one pooled connection, and the 8/9/10 ordinals are otherwise unexercised. - Build
Lite.Testson Windows and runFactsNameQueriesTestsplusQuerySpillsDistinctQueryCountTests, which now also traverses the reader close. - Read one real
analyze_serverand oneget_analysis_factspayload for aQUERY_SPILLSfinding and judge howvalue(window total) reads besideranked[0].value(one query). - Re-time the ranked read where a four-hour window spills across thousands of distinct
(database, query_hash)pairs.
|
Follow-up pass (base 23bacbd → head 398e3d4).
Per this pass's instructions, a merge's incoming content isn't the author's change unless a new commit interacts with it, and none does here. So this pass has nothing of this PR's own to review, and the verdict below reflects only that. It does not mean the prior full review's CHANGES_REQUESTED findings (the |
There was a problem hiding this comment.
Verdict: LGTM
Claims
- None in scope — the PR body was not revised in this range, and its claims about the PARAMETER_SENSITIVITY/QUERY_SPILLS work were already graded in the prior full-pass review (head=23bacbd482c863cace9cb4e532729a7bba402fc3). This pass's only commit range content (see Riskiest lines) carries no body claims of its own.
Riskiest lines
- None in scope — the one file difference in this range,
Darling/tools/provision-roles.sql:3011-3015, was brought in by398e3d486's merge oforigin/dev(commit0a6121e1c, PR #6644), not authored by this PR, and no commit in this range interacts with it. Checked anyway: the three signatures (update_notification_route,set_notification_route_agent,set_notification_route_destinations) match the file's ownCREATE OR REPLACE FUNCTION/REVOKE/GRANT EXECUTEstatements exactly — not a defect.
Traps near this diff
- None evaluated — no author-authored change is in scope this pass (see Riskiest lines above).
Divergence (blind read vs PR body)
None — the PR body was not amended in this range, so there is nothing new to compare it against.
Reviewer checklist
- Confirm a future commit addresses the prior full review's unresolved CHANGES_REQUESTED findings (head=23bacbd482c863cace9cb4e532729a7bba402fc3): the
Rankedinvariant mismatch onQUERY_SPILLS, the brokenFactRankedTestssource-pin assertion on both SKUs, and the undocumentedobject_namefield — this pass's only in-range content is an unrelated dev merge and does not touch them.
…chSubject call shape Lite: SUM(BIGINT) is a HUGEINT, which DuckDB.NET returns as BigInteger. The new QUERY_SPILLS ranked read used Convert.ToDouble on it, which throws InvalidCastException; CollectQueryStatsFactsAsync caught it and emitted no QUERY_STATS facts at all (spills and high-DOP). Read it with ToInt64, the idiom the aggregate above already uses. Darling.Tests: FactRankedTests pinned the literal FactRanked.Attach(new call shape; update it to FactRanked.Attach(FactRanked.AttachSubject(new.
…facts, document object_name QUERY_SPILLS keeps Value as the window total, so its ranked[0] value is the worst query's own pages. The RankedObject invariant and the get_analysis_facts descriptions now say so. FactIdentity lists QUERY_SPILLS and PARAMETER_SENSITIVITY, so a consumed hop still names its query. Both tool descriptions document object_name and the new root_fact.ranked cases. The Fact.Ranked doc no longer says no SQL Server collector sets it. The parameter sensitivity read gets a full tiebreak so the named plans are stable. Darling's spill SQL constant is private and Lite's field sits above its own summary.
Claude posting for Erik Darling. Answered in 53778c2. The QUERY_SPILLS value exception is now stated in the invariant doc and tool descriptions. FactIdentity gained both keys. object_name is documented. Tiebreak and cosmetic fixes are in. Details are in the PR body under "Review round".
| } | ||
|
|
||
| [McpServerTool(Name = "get_analysis_facts"), Description("Raw scored facts from the inference engine's pipeline, WITHOUT graph traversal: facts, not findings. Includes ANOMALY_* facts with gate metadata even when they fired but stayed under the finding floor, so a fact here did not necessarily produce a finding. baseline_confidence (a fact's baseline trustworthiness) differs from a finding's confidence in analyze_server. collection_caveats is absent on a clean read; when present, the families it names were NOT read, so their absence is not evidence they were quiet. ranked is absent unless 2+ objects ride. <<GUIDE>> Exposes the raw scored facts from the inference engine's collect+score pipeline WITHOUT graph traversal. Shows every observation the engine sees: wait stats as fraction-of-period, blocking rates, config settings, memory stats, plus base severity, final severity after amplifiers, and which amplifiers matched. The anomaly detector runs on this read too, so the ANOMALY_* facts the full pass would score are here with the gate metadata the pass scored them on (deviation_sigma against fire_threshold, baseline_samples, baseline_tier, baseline_low_quality), including the ones that fired but stayed under the finding floor; the cost is the detector's baseline reads on top of the collector's. For ANOMALY_* facts the metadata carries baseline_confidence — the baseline's own trustworthiness (tier x sample density), which the scorer multiplies into that fact's severity; it is a different quantity from a finding's confidence in analyze_server. Use this to understand exactly what the engine is working with, or to investigate facts that didn't reach the severity threshold for findings. When one or more fact families could not be read (a timed-out, cancelled or failed collector read), the payload carries collection_caveats (families_failed, families_total, entries[{family, read, outcome, message}]) and the caveat says so; the field is absent on a clean read, and a fact set missing those families is not evidence that they were quiet. A fact whose read ranked several objects carries ranked: the objects it ranks, worst first, as [{object_name, database_name, value, figures}] with the family's own un-prefixed metadata names as the figure keys, entry [0] being the fact's own subject (same object_name and value). Absent unless two or more objects ride, and capped at three - the per-object lists are the get_pg_* reads. The PLAN_REGRESSION fact leaves out a regression whose statement is OPTION (RECOMPILE), whose newer plan returns 10x the rows per call with the cost gone per row, or whose two plans were compiled for different parameter values over unchanged statistics, and counts them in cross_input_excluded_count. Its worst_input_basis says how the worst kept offender's two plans were compared: 1 both plans were compiled for the same parameter values, 2 the compiled values differ and the statistics were updated between the compiles (a possible statistics-update flip), 0 not checked (a plan or the statement text was missing or withheld). source accepts: anomaly, bad_actor, blocking, config, coverage, cpu, database_config, disk, io, jobs, memory, pg_bloat, pg_blocking, pg_buffer, pg_config, pg_cpu, pg_database, pg_growth, pg_io, pg_kernel, pg_memory, pg_plans, pg_posture, pg_queries, pg_replication, pg_sessions, pg_temp, pg_vacuum, pg_waits, pg_write, queries, sessions, tempdb, waits; omit it for all. total_facts counts every scored fact before the source and min_severity filters; shown counts the facts that passed them; filters echoes the two values applied.")] | ||
| [McpServerTool(Name = "get_analysis_facts"), Description("Raw scored facts from the inference engine's pipeline, WITHOUT graph traversal: facts, not findings. Includes ANOMALY_* facts with gate metadata even when they fired but stayed under the finding floor, so a fact here did not necessarily produce a finding. baseline_confidence (a fact's baseline trustworthiness) differs from a finding's confidence in analyze_server. collection_caveats is absent on a clean read; when present, the families it names were NOT read, so their absence is not evidence they were quiet. ranked is absent unless 2+ objects ride. QUERY_SPILLS and PARAMETER_SENSITIVITY facts also carry object_name even for one query: the worst query as database/query_hash, or database/query_hash/query_plan_hash. <<GUIDE>> Exposes the raw scored facts from the inference engine's collect+score pipeline WITHOUT graph traversal. Shows every observation the engine sees: wait stats as fraction-of-period, blocking rates, config settings, memory stats, plus base severity, final severity after amplifiers, and which amplifiers matched. The anomaly detector runs on this read too, so the ANOMALY_* facts the full pass would score are here with the gate metadata the pass scored them on (deviation_sigma against fire_threshold, baseline_samples, baseline_tier, baseline_low_quality), including the ones that fired but stayed under the finding floor; the cost is the detector's baseline reads on top of the collector's. For ANOMALY_* facts the metadata carries baseline_confidence — the baseline's own trustworthiness (tier x sample density), which the scorer multiplies into that fact's severity; it is a different quantity from a finding's confidence in analyze_server. Use this to understand exactly what the engine is working with, or to investigate facts that didn't reach the severity threshold for findings. When one or more fact families could not be read (a timed-out, cancelled or failed collector read), the payload carries collection_caveats (families_failed, families_total, entries[{family, read, outcome, message}]) and the caveat says so; the field is absent on a clean read, and a fact set missing those families is not evidence that they were quiet. A fact whose read ranked several objects carries ranked: the objects it ranks, worst first, as [{object_name, database_name, value, figures}] with the family's own un-prefixed metadata names as the figure keys, entry [0] being the fact's own subject (same object_name, and the same value except on QUERY_SPILLS, where it is that query's own spilled pages and the fact value is the window total). Absent unless two or more objects ride, and capped at three - the per-object lists are the get_pg_* reads. The PLAN_REGRESSION fact leaves out a regression whose statement is OPTION (RECOMPILE), whose newer plan returns 10x the rows per call with the cost gone per row, or whose two plans were compiled for different parameter values over unchanged statistics, and counts them in cross_input_excluded_count. Its worst_input_basis says how the worst kept offender's two plans were compared: 1 both plans were compiled for the same parameter values, 2 the compiled values differ and the statistics were updated between the compiles (a possible statistics-update flip), 0 not checked (a plan or the statement text was missing or withheld). source accepts: anomaly, bad_actor, blocking, config, coverage, cpu, database_config, disk, io, jobs, memory, pg_bloat, pg_blocking, pg_buffer, pg_config, pg_cpu, pg_database, pg_growth, pg_io, pg_kernel, pg_memory, pg_plans, pg_posture, pg_queries, pg_replication, pg_sessions, pg_temp, pg_vacuum, pg_waits, pg_write, queries, sessions, tempdb, waits; omit it for all. total_facts counts every scored fact before the source and min_severity filters; shown counts the facts that passed them; filters echoes the two values applied.")] |
There was a problem hiding this comment.
The new head sentence puts get_analysis_facts over the 620-character served-head cap, on both SKUs.
The sentence added before <<GUIDE>> —
QUERY_SPILLS and PARAMETER_SENSITIVITY facts also carry object_name even for one query: the worst query as database/query_hash, or database/query_hash/query_plan_hash.
— is 168 characters. The head (everything before the marker, after TrimEnd) goes from 554 to 722 characters, and McpToolGuide.Served appends the 31-character GuidePointer, so the served head goes from 585 to 753.
Two pins cap that at 620, per SKU:
Lite.Tests/McpToolGuideHeads.SqlCore.cs:89—EveryConvertedHead_CarriesItsGuardrailFact_AndThePointeriteratesConvertedTools, which includes"get_analysis_facts"(line 31), and assertsserved.Served.Length <= 620.Lite.Tests/McpToolGuideTests.cs:50—EveryConvertedHead_StaysAtOrUnder620Characters, the roster-free backstop over every tool with a tail.
The Darling copy of the same description (Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpTools.cs:312) is byte-identical at 722 characters, so its twins fail too: Darling/Darling.Tests/McpToolGuideHeads.SqlCore.cs:90 and Darling/Darling.Tests/McpToolGuideTests.cs:50. Four failing tests, not one.
This family has no slack: analyze_server serves at 617 and get_analysis_findings at 610, both against the same 620 bar — which is why the PR body's "Other pins checked" census (ParameterSensitivityFiringSignatureParityTests, QueryHighDopStaleMaxDopParityTests, …) missing these two is the gap that matters. Darling.Tests and Lite.Tests building to 0 errors does not reach a length assertion.
The 1,000-character absolute cap in McpToolsListBudgetTests (ConvertedHeadCap) still passes at 753, so this fails only on the 620 target — the one the family is actually held to.
The field is worth documenting; the head is not where it fits. The <<GUIDE>> tail already carries the full statement ("entry [0] being the fact's own subject (same object_name, and the same value except on QUERY_SPILLS …)"), and the tail has ~21k characters of headroom against MaxGuideCharacters. Either move the object_name sentence into the tail, or compress it to a head-sized clause (e.g. object_name rides on QUERY_SPILLS and PARAMETER_SENSITIVITY even for one query. — 73 characters, which lands the served head at 658, still over; so it needs to come out of the head, or something else in the head has to go). Whichever way, re-measure both SKUs against 620 rather than against the 1,000 cap.
| } | ||
|
|
||
| [McpServerTool(Name = "get_analysis_facts"), Description("Raw scored facts from the inference engine's pipeline, WITHOUT graph traversal: facts, not findings. Includes ANOMALY_* facts with gate metadata even when they fired but stayed under the finding floor, so a fact here did not necessarily produce a finding. baseline_confidence (a fact's baseline trustworthiness) differs from a finding's confidence in analyze_server. collection_caveats is absent on a clean read; when present, the families it names were NOT read, so their absence is not evidence they were quiet. ranked is absent unless 2+ objects ride. <<GUIDE>> Exposes the raw scored facts from the inference engine's collect+score pipeline WITHOUT graph traversal. Shows every observation the engine sees: wait stats as fraction-of-period, blocking rates, config settings, memory stats, plus base severity, final severity after amplifiers, and which amplifiers matched. The anomaly detector runs on this read too, so the ANOMALY_* facts the full pass would score are here with the gate metadata the pass scored them on (deviation_sigma against fire_threshold, baseline_samples, baseline_tier, baseline_low_quality), including the ones that fired but stayed under the finding floor; the cost is the detector's baseline reads on top of the collector's. For ANOMALY_* facts the metadata carries baseline_confidence — the baseline's own trustworthiness (tier x sample density), which the scorer multiplies into that fact's severity; it is a different quantity from a finding's confidence in analyze_server. Use this to understand exactly what the engine is working with, or to investigate facts that didn't reach the severity threshold for findings. When one or more fact families could not be read (a timed-out, cancelled or failed collector read), the payload carries collection_caveats (families_failed, families_total, entries[{family, read, outcome, message}]) and the caveat says so; the field is absent on a clean read, and a fact set missing those families is not evidence that they were quiet. A fact whose read ranked several objects carries ranked: the objects it ranks, worst first, as [{object_name, database_name, value, figures}] with the family's own un-prefixed metadata names as the figure keys, entry [0] being the fact's own subject (same object_name and value). Absent unless two or more objects ride, and capped at three - the per-object lists are the get_pg_* reads. The PLAN_REGRESSION fact leaves out a regression whose statement is OPTION (RECOMPILE), whose newer plan returns 10x the rows per call with the cost gone per row, or whose two plans were compiled for different parameter values over unchanged statistics, and counts them in cross_input_excluded_count. Its worst_input_basis says how the worst kept offender's two plans were compared: 1 both plans were compiled for the same parameter values, 2 the compiled values differ and the statistics were updated between the compiles (a possible statistics-update flip), 0 not checked (a plan or the statement text was missing or withheld). source accepts: anomaly, bad_actor, blocking, config, coverage, cpu, database_config, disk, io, jobs, memory, pg_bloat, pg_blocking, pg_buffer, pg_config, pg_cpu, pg_database, pg_growth, pg_io, pg_kernel, pg_memory, pg_plans, pg_posture, pg_queries, pg_replication, pg_sessions, pg_temp, pg_vacuum, pg_waits, pg_write, queries, sessions, tempdb, waits; omit it for all. total_facts counts every scored fact before the source and min_severity filters; shown counts the facts that passed them; filters echoes the two values applied.")] | ||
| [McpServerTool(Name = "get_analysis_facts"), Description("Raw scored facts from the inference engine's pipeline, WITHOUT graph traversal: facts, not findings. Includes ANOMALY_* facts with gate metadata even when they fired but stayed under the finding floor, so a fact here did not necessarily produce a finding. baseline_confidence (a fact's baseline trustworthiness) differs from a finding's confidence in analyze_server. collection_caveats is absent on a clean read; when present, the families it names were NOT read, so their absence is not evidence they were quiet. ranked is absent unless 2+ objects ride. QUERY_SPILLS and PARAMETER_SENSITIVITY facts also carry object_name even for one query: the worst query as database/query_hash, or database/query_hash/query_plan_hash. <<GUIDE>> Exposes the raw scored facts from the inference engine's collect+score pipeline WITHOUT graph traversal. Shows every observation the engine sees: wait stats as fraction-of-period, blocking rates, config settings, memory stats, plus base severity, final severity after amplifiers, and which amplifiers matched. The anomaly detector runs on this read too, so the ANOMALY_* facts the full pass would score are here with the gate metadata the pass scored them on (deviation_sigma against fire_threshold, baseline_samples, baseline_tier, baseline_low_quality), including the ones that fired but stayed under the finding floor; the cost is the detector's baseline reads on top of the collector's. For ANOMALY_* facts the metadata carries baseline_confidence — the baseline's own trustworthiness (tier x sample density), which the scorer multiplies into that fact's severity; it is a different quantity from a finding's confidence in analyze_server. Use this to understand exactly what the engine is working with, or to investigate facts that didn't reach the severity threshold for findings. When one or more fact families could not be read (a timed-out, cancelled or failed collector read), the payload carries collection_caveats (families_failed, families_total, entries[{family, read, outcome, message}]) and the caveat says so; the field is absent on a clean read, and a fact set missing those families is not evidence that they were quiet. A fact whose read ranked several objects carries ranked: the objects it ranks, worst first, as [{object_name, database_name, value, figures}] with the family's own un-prefixed metadata names as the figure keys, entry [0] being the fact's own subject (same object_name, and the same value except on QUERY_SPILLS, where it is that query's own spilled pages and the fact value is the window total). Absent unless two or more objects ride, and capped at three - the per-object lists are the get_pg_* reads. The PLAN_REGRESSION fact leaves out a regression whose statement is OPTION (RECOMPILE), whose newer plan returns 10x the rows per call with the cost gone per row, or whose two plans were compiled for different parameter values over unchanged statistics, and counts them in cross_input_excluded_count. Its worst_input_basis says how the worst kept offender's two plans were compared: 1 both plans were compiled for the same parameter values, 2 the compiled values differ and the statistics were updated between the compiles (a possible statistics-update flip), 0 not checked (a plan or the statement text was missing or withheld). source accepts: anomaly, bad_actor, blocking, config, coverage, cpu, database_config, disk, io, jobs, memory, pg_bloat, pg_blocking, pg_buffer, pg_config, pg_cpu, pg_database, pg_growth, pg_io, pg_kernel, pg_memory, pg_plans, pg_posture, pg_queries, pg_replication, pg_sessions, pg_temp, pg_vacuum, pg_waits, pg_write, queries, sessions, tempdb, waits; omit it for all. total_facts counts every scored fact before the source and min_severity filters; shown counts the facts that passed them; filters echoes the two values applied.")] |
There was a problem hiding this comment.
Darling's half of the head-budget break (see the full accounting on Lite/Mcp/McpAnalysisTools.cs:278): this description is byte-identical to Lite's, so its head is also 722 characters and its served head 753, against the 620 target asserted at Darling/Darling.Tests/McpToolGuideHeads.SqlCore.cs:90 (get_analysis_facts is in that file's ConvertedTools at line 31) and at Darling/Darling.Tests/McpToolGuideTests.cs:50.
Parity is held correctly here — both SKUs carry the same sentence — so whatever shortens Lite's head has to shorten this one by the same bytes, or the byte-identity the family relies on breaks instead.
| /// <c>RUNNING_JOBS</c> the Agent job running longest past its history (<see cref="Fact.ObjectName"/>, | ||
| /// #3693); the two object anomalies the table they observed. Today's SQL Server graph reaches the first two | ||
| /// #3693); the two object anomalies the table they observed; the two query facts the query (or plan) they rank | ||
| /// first, as <c>database/query_hash</c> or <c>database/query_hash/query_plan_hash</c> (#6640). Today's SQL Server graph reaches the first two |
There was a problem hiding this comment.
Non-blocking, but this roster comment now misdescribes the two keys the same hunk added. The clause still reads "the others are rostered because they NAME something … not because an edge reaches them today" — and edges do reach both of them:
RelationshipGraph.cs:198RESOURCE_SEMAPHORE → QUERY_SPILLS,:208MEMORY_GRANT_PENDING → QUERY_SPILLS,:403TEMPDB_USAGE → QUERY_SPILLSRelationshipGraph.cs:168CPU_SPIKE → PARAMETER_SENSITIVITY,:223MEMORY_GRANT_PENDING → PARAMETER_SENSITIVITY
So the Behind it: sentence is live for these two, not rostered-but-dormant, which is the opposite of what the sentence tells the next reader. That matters more than usual here because this file's own summary calls a roster row "the reviewer's prompt to read that row", and .github/REVIEW-TRAPS.md has the inverse shape on record ("A key in the graph is not a key in the data; grep the collector that emits it", #3709) — a reader who trusts this clause would conclude the arms are inert and stop checking them.
Worth noting alongside it that FactScorer.cs:510 thresholds QUERY_SPILLS at 100/1000 pages, so a spilling window usually scores it at or above 0.5 and BuildStories takes it as its own root before a lower-severity root can consume it as a hop (the #3688/#3709 semantics). The hop sentence is therefore the uncommon path for this key and the card's own new Most spilled: sentence is the common one — both correct, but the comment reads as though only the hop path existed.
| /// query has no <c>ranked</c> array (it rides at two or more), and <c>get_analysis_facts</c> carries no <c>object_name</c> | ||
| /// for any fact. These two keys get it, so every other fact's payload keeps its bytes.</summary> | ||
| private static bool NamesItsSubjectInPayload(string key) => | ||
| key is "QUERY_SPILLS" or "PARAMETER_SENSITIVITY"; |
There was a problem hiding this comment.
Non-blocking: the QUERY_SPILLS value exception is stated in three of the four places that assert it, and the fourth is in this file.
Stated: AnalysisModels.cs:13 (the RankedObject invariant), and both SKUs' get_analysis_facts descriptions ("the same value except on QUERY_SPILLS, where it is that query's own spilled pages and the fact value is the window total").
Not stated: ToPayload's own doc, 30 lines above this at FactRankedPayload.cs:47 — "Entry [0] is the subject, and it is emitted. It repeats ObjectName / value that the enclosing payload already carries, on purpose … The invariant (RankedObject) is what makes the repetition safe — the two cannot disagree." For QUERY_SPILLS they now do disagree, and this is the paragraph a reader lands on when they are deciding whether ranked[0].value may be read as the card's own value. One clause pointing at the RankedObject exception closes it.
Same gap on the analyze_server side, where the body says it chose not to add object_name to root_fact: root_fact there is { key, value } plus ranked, so a QUERY_SPILLS finding emits root_fact.value = window total next to root_fact.ranked[0].value = one query's pages, and that description's ranked paragraph makes no value claim either way. Not wrong, just the one surface where an agent sees the two numbers side by side with nothing saying they are different quantities.
Also noting for the record that FactRankedTests.AssertInvariant (Darling.Tests/FactRankedTests.cs:52) still asserts fact.Value == fact.Ranked[0].Value. That is harmless today — its five callers are all PgTarget* families — and the PR body says so explicitly under "Considered, not added". Flagging only because the helper's own doc calls itself "the invariant, in one place", which is now one place out of two.
| var spillRanked = new List<RankedObject>(); | ||
| if (totalSpills > 0) | ||
| { | ||
| await reader.CloseAsync(); |
There was a problem hiding this comment.
Non-blocking, and an asymmetry with the Lite twin worth a sentence in the comment rather than a code change.
The aggregate and the ranked read are two statements on the same connection with the first reader closed between them, so on PostgreSQL they run in two implicit transactions. Lite's twin holds _duckDb.AcquireReadLock(...) for the whole method (Lite/Analysis/DuckDbFactCollector.QueryPerf.cs:41), so no collector write can land between its two reads; nothing here does the equivalent.
The window is [TimeRangeStart, TimeRangeEnd] and on an unanchored pass TimeRangeEnd is ~now, so a collection that commits between the two statements with collection_time <= TimeRangeEnd is admitted by the second read and not the first. If it carries spills for the current top query, ranked[0].Value can come back greater than Value — which is exactly the one thing AnalysisModels.cs:13 promises cannot happen ("never more than the total").
Narrow and self-healing (the next pass re-reads both), and the figures the card scores on are unaffected. But the invariant is now written down as an absolute, so either the window should be closed to the aggregate's own snapshot (a single REPEATABLE READ transaction around both reads, or reading the two from one statement) or the doc should say "never more than the total within one read". The comment above currently says "Same window, same filter" — true of the SQL, not quite true of the instant.
|
|
||
| try | ||
| { | ||
| var periodEnd = DateTime.SpecifyKind(new DateTime(DateTime.UtcNow.Ticks - (DateTime.UtcNow.Ticks % TimeSpan.TicksPerSecond)), DateTimeKind.Unspecified); |
There was a problem hiding this comment.
Non-blocking: this reads the clock twice in one expression, and its own Lite twin reads it once.
DateTime.UtcNow.Ticks - (DateTime.UtcNow.Ticks % TimeSpan.TicksPerSecond)
The two UtcNow reads are independent, so if the second lands in the next second the subtraction is not a second-truncation at all (first read 10.999 s, second 11.001 s → 10.998 s). The Lite twin does it correctly off one read: Lite.Tests/FactsNameQueriesTests.cs:28-30, DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond * TimeSpan.TicksPerSecond.
It happens not to bite: every seed and both window bounds are derived from the single periodEnd value once it exists, and nothing asserts the truncation, so an untruncated anchor changes no assertion. Flagging it because it is .github/REVIEW-TRAPS.md's recorded shape verbatim — "A test that reads the clock twice measures the CI scheduler … One anchor owns both ends of an interval" (#3676) — and this is a live test whose future edits will inherit the anchor. Copying the Lite twin's expression costs nothing and takes the shape off the board.
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
Claims
- VERIFIED — PARAMETER_SENSITIVITY sets
ObjectName/Ranked, nameddatabase/query_hash/query_plan_hash—PgFactCollector.QueryPerf.cs:243-245,:308-321; Lite:260-262,:318-331 - VERIFIED —
Valueis the worker ratio,Figuresthe four keys,Metadataunchanged —PgFactCollector.QueryPerf.cs:314-321vs:338-347 - VERIFIED — a second statement ranks
(database, query_hash)by spilled pages, top three, only when the aggregate found spills — Darling:68-81under:110; Lite:18-31under:107 - VERIFIED — QUERY_SPILLS keeps the window total as
Value, the one stated exception —:133,AnalysisModels.cs:11-15, pinnedLite.Tests/FactsNameQueriesTests.cs:116 - VERIFIED — the card text names the queries for both facts —
FactAdvice.cs:1640,:1726via:1661-1666; assertedFactsNameQueriesAdviceTests.cs:60,:74 - VERIFIED —
AttachSubjectaddsobject_namefor these two keys only, other facts keep their bytes, both tools call it —FactRankedPayload.cs:77-95;DarlingMcpTools.cs:429/438,McpAnalysisTools.cs:363/390;Assert.SameatFactsNameQueriesAdviceTests.cs:101-102 - VERIFIED — Lite has the same change; one cap,
FactRanked.MaxObjects - VERIFIED — no migration rung — no schema file in the diff;
Rankedrides in memory only (InferenceEngine.cs:383) - VERIFIED — the ranked read uses
ToInt64— Lite:120→BlockingPairRowQuery.cs:96-98 - VERIFIED — both SKUs end
ORDER BY worker_ratio DESC, database_name, query_hash, query_plan_hash— Darling:253, Lite:270; pinned byParameterSensitivity_TiedRatios_AreNamedInKeyOrder - VERIFIED — the two keys joined
SqlServerKeyswithDescribearms, Lite roster updated —FactIdentity.cs:83-84,:159-162;StoryNamedHopsTests.cs:36,:60-64; one reader outside the file (:121), both re-read - VERIFIED — the
Fact.Rankeddoc no longer says no SQL Server collector sets it —AnalysisModels.cs:72-76 - VERIFIED — field placement and the non-
publicconstant fixed on both SKUs —:68/:18 - VERIFIED —
FactRankedTests.BothToolFiles_...updated to the new shape —:154-155matches both tool files - VERIFIED —
root_factcarries noobject_name—DarlingMcpTools.cs:251-254,McpAnalysisTools.cs:217-220 - VERIFIED but incomplete — "Other pins checked": the six named pins do hold (the firing-signature regex covers only the unchanged
WHERE; the high-DOP guard is keyed onCOUNT(... max_dop ...)). The census never reaches the head-budget pins this commit breaks — see REFUTED - UNVERIFIED — the seeded timings (158 ms PG, 3-5 ms DuckDB, aggregate 285-317 ms) — needs the million-row rig
- UNVERIFIED — the two production timings — production reads
- UNVERIFIED — "Npgsql returns
SUM(bigint)asnumeric/decimal" — the code takes that path (:122) and the reasoning is sound, but the CLR type needs a live PostgreSQL; the only test skips withoutDARLING_TEST_PG(FactsNameQueriesLiveTests.cs:200) - UNVERIFIED — the deleted scratch harnesses (PG 16 run, the DuckDB.NET
BigIntegerreproduction) - UNVERIFIED — "#6643 … each test above should fail there" — another PR's run
- REFUTED — "build to 0 errors" as a sufficient check on the description edit: the 168-char sentence added before
<<GUIDE>>takes theget_analysis_factsserved head from 585 to 753 against a 620 cap asserted four times —Lite.Tests/McpToolGuideHeads.SqlCore.cs:89,Lite.Tests/McpToolGuideTests.cs:50,Darling.Tests/McpToolGuideHeads.SqlCore.cs:90,Darling.Tests/McpToolGuideTests.cs:50. A length assertion is not a compile error. Detail inline onMcpAnalysisTools.cs:278 - REFUTED — the table's "(2)" for Lite
FactsNameQueriesTestsand "(3)" forFactsNameQueriesAdviceTests— the files carry 3 and 4[Fact]methods; stale after the review round added two tests the later sections do name
Riskiest lines
Lite/Mcp/McpAnalysisTools.cs:278(and byte-identicalDarlingMcpTools.cs:312) — the only line breaking a shipped assertion, in a family with ~3 characters of slack (analyze_server617,get_analysis_findings610, cap 620) — covered byEveryConvertedHead_CarriesItsGuardrailFact_AndThePointerandEveryConvertedHead_StaysAtOrUnder620Characters, both SKUs; all four fail todayPgFactCollector.QueryPerf.cs:112— closing the aggregate's reader mid-method and running a second command on the connection; a throw hits thecatchat:164and takesQUERY_HIGH_DOPdown with QUERY_SPILLS, exactly the LiteBigIntegerfailure in run 39981348396 — UNCOVERED (FactsNameQueriesLiveTestsskips withoutDARLING_TEST_PG)Lite/Analysis/DuckDbFactCollector.QueryPerf.cs:344/PgFactCollector.QueryPerf.cs:336—ranked[0]indexed unguarded, safe only because every row incrementingoffenderCountalso appends whileCount < 3— covered on Lite byParameterSensitivity_NamesTheOffenders_WorstFirst_WithinTheCap; UNCOVERED on Darling
Traps near this diff
- "Lite and Darling ship in parity … a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit" — does not apply — both collectors (
:68/:18), both tool files and the shared halves move together; descriptions byte-identical at 722 chars - "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted (#3684). Alias, do not copy." — does not apply — no test pins the two
QuerySpillsRankedSqlequal, and the aggregate above is already per-SKU (v_server_configLite:65vsserver_configDarling:40) - "A count taken off the page and named as the population's (#3679)" — does not apply —
spilling_query_countstill comes from the un-limitedCOUNT(DISTINCT …)at:54-55 - "Truncation inferred from a full page … Fetch
limit + 1" — does not apply — notruncatedflag; the cap is a stated constant (AnalysisModels.cs:46) - "
ORDER BY 3 + 4 DESC… Sort by named columns." — does not apply — both statements sort by named columns (:80) - "Integer division in a rate column … suspect until a
1.0 *or a CAST is found" — does not apply — the only division is the pre-existing cast-and-NULLIF'd ratio at:237; the new read only sums - "
CASE WHEN interval > 0 THEN delta / interval ELSE 0 ENDspells 'unknowable' as 0 … End atEND(NULL)" — does not apply — no new rate orCASE; the0.0fallbacks sit on a sum whose row exists by construction (delta_spills > 0) - "A test that reads the clock twice measures the CI scheduler … One anchor owns both ends of an interval (#3676)" — applies —
FactsNameQueriesLiveTests.cs:101readsUtcNow.Tickstwice where the Lite twin (FactsNameQueriesTests.cs:28-30) reads it once; no assertion depends on it, so non-blocking, noted inline - "A NEW string constant in
PgTargetFactKeyswithout aByFactKeyrow (#3688)" — does not apply — no key added; both are pre-existing literals (FactScorer.cs:510,:515) - "
PopulateStoryTextruns BEFOREClusterIntoIncidents/StampClusters/Reconcile… a sentence added after folding must re-read the frozenStoryTextblob (#3709)" — does not apply — appended inside the composers (FactAdvice.cs:1640,:1726) - "A key in the graph is not a key in the data; grep the collector that emits it (#3709)" — does not apply as dormancy — both keys are emitted here and reached by live edges (
RelationshipGraph.cs:198,:208,:403,:168,:223) — butFactIdentity.cs:72now asserts the opposite, flagged inline - "Never answer a trap with a test that reads source text or asserts how code is spelled … Never add one (#5833)" — does not apply — none added;
FactRankedTests.cs:154-155is an existing pin kept in step
Divergence (blind read vs PR body)
One divergence, and it is the finding. The body's "CI fix" and "Other pins checked" census covers the source-text pins that read the collector files, and it is right about every pin it names — but the commit's other half edits a tool description, and that surface has its own pinned budget the census never visits. My blind read measured the get_analysis_facts head at 722 characters against a 620-character served-head cap asserted four times across the two SKUs; "build to 0 errors" cannot see a length assertion. The body treats "object_name undocumented: real" as closed by adding a sentence to the head without stating what the head then measures.
Two smaller disagreements, both understating the body's own work: the table's "(2)" and "(3)" are now 3 and 4, and FactIdentity.cs:72 claims no edge reaches the newly rostered keys when five do. Everything else the body claims about the diff I confirmed from the tree, including the three things it declines to do.
Reviewer checklist
- Run
Lite.TestsandDarling.Testson Windows and confirm the four head-budget tests pass once the description is trimmed — the figures above are a character count, not a test run - Run
FactsNameQueriesLiveTestswithDARLING_TEST_PGset: the only coverage for Darling's ranked read, the three-column projection offFROM latest, svr, andConvert.ToDoubleoverSUM(bigint) - Call
get_analysis_factsagainst a real spilling window:object_nameon the two facts,rankedabsent at one object, no other fact's bytes moved - Confirm #6643's revert run actually fails each new test
The tool guide split now caps each served head at 620 characters. The new object_name sentence put the get_analysis_facts head at 753. The sentence now opens nothing in the head and closes the guide tail instead, in both products, so the head is back under the cap and the guardrail facts stay.
| /// <c>RUNNING_JOBS</c> the Agent job running longest past its history (<see cref="Fact.ObjectName"/>, | ||
| /// #3693); the two object anomalies the table they observed. Today's SQL Server graph reaches the first two | ||
| /// #3693); the two object anomalies the table they observed; the two query facts the query (or plan) they rank | ||
| /// first, as <c>database/query_hash</c> or <c>database/query_hash/query_plan_hash</c> (#6640). Today's SQL Server graph reaches the first two |
There was a problem hiding this comment.
The roster doc is false for the two keys this PR adds, and that hides a behaviour change.
The sentence this hunk leads into says the keys other than BLOCKING_CHAIN / RUNNING_JOBS are rostered "because they NAME something … not because an edge reaches them today." Six edges in today's SQL Server graph reach the two keys this PR just added:
RelationshipGraph.cs:198RESOURCE_SEMAPHORE → QUERY_SPILLSRelationshipGraph.cs:208MEMORY_GRANT_PENDING → QUERY_SPILLSRelationshipGraph.cs:403TEMPDB_USAGE → QUERY_SPILLSRelationshipGraph.cs:440PARAMETER_SENSITIVITY → QUERY_SPILLSRelationshipGraph.cs:168CPU_SPIKE → PARAMETER_SENSITIVITYRelationshipGraph.cs:223MEMORY_GRANT_PENDING → PARAMETER_SENSITIVITY
So the roster edit is not inert the way the comment implies. InferenceEngine.cs:356 calls FactIdentity.NamedHopsOf on every story path, and FactAdvice.cs:355 (WithNamedHops) appends one Behind it: … sentence per named hop to the root's investigation. From this PR forward, a story rooted at MEMORY_GRANT_PENDING, RESOURCE_SEMAPHORE, TEMPDB_USAGE or CPU_SPIKE that consumes either query fact below the 0.5 self-root floor gains a sentence it did not have — e.g. Behind it: `QUERY_SPILLS` (severity 0.40) — most spilled query `Sales/0xAB`: 1,100 pages (8.59 MB) spilled to tempdb — memory grants too small for their sorts/hashes. That text is composed by PopulateStoryText and frozen into the persisted finding's StoryText, so it reaches the viewer, the alert e-mail, analyze_server and get_analysis_findings on every existing install — not just the two cards this PR is about. MaxNamedHops is 3, so on a crowded path the two new names can also displace a BLOCKING_CHAIN or RUNNING_JOBS name that used to ride.
That may well be the behaviour you want, but the comment states the opposite of what the graph does, and neither the comment nor the PR body mentions the root-card text change. Please fix the sentence to say these two ARE reached as hops today and name the edges, and say in the body that root cards gain the sentence.
Nothing asserts the new sentence or its absence today, so this is also the one place a behaviour test would pay: a story rooted at MEMORY_GRANT_PENDING with a sub-0.5 QUERY_SPILLS on the path, asserting the root's investigation names the query.
| /// <summary>#6640: the queries behind QUERY_SPILLS, most spilled pages first. Same window, same filter and same | ||
| /// (database, query_hash) identity as the aggregate above, so the names are exactly the queries its spilling_query_count counts. | ||
| /// Run only when the aggregate found spills, and capped at the ranked-list cap.</summary> | ||
| private static readonly string QuerySpillsRankedSql = @" |
There was a problem hiding this comment.
Non-blocking, but the doc on PgFactCollector.AllSql (PgFactCollector.cs:294-302) opens with "Every query this collector executes, for the ungated dialect/hygiene pins in Darling.Tests". QuerySpillsRankedSql is executed by this collector (line 113) and is not in that list, so that sentence is now false.
No live consequence here — I checked this statement against what those pins assert and it passes all of them: v_query_stats is a V4 passthrough view, parameters are $1–$3 positional, there is no QUALIFY, no bare NOW()/CURRENT_TIMESTAMP, no read_parquet, no any_value. The cost is forward-looking: the next edit to this statement is unguarded, and a reader who trusts the roster's own sentence will believe it is.
Note that registering it is not free — PgFactCollectorTests.AllSql_CoversEveryQuery_OnePerCollectMethodPlusTheDmvFallback pins AllSql.Count == LiteCollectMethodSurface.Length + 4, so adding the entry means moving that arithmetic and its comment too. Amending the AllSql sentence to say "one per collect method, plus the conditional second reads" would be the cheaper honest fix if you would rather not grow the roster.
| var db = rankReader.IsDBNull(0) ? string.Empty : rankReader.GetString(0); | ||
| var hash = rankReader.IsDBNull(1) ? string.Empty : rankReader.GetString(1); | ||
| var pages = rankReader.IsDBNull(2) ? 0.0 : Convert.ToDouble(rankReader.GetValue(2), System.Globalization.CultureInfo.InvariantCulture); | ||
| spillRanked.Add(new RankedObject(db + "/" + hash, db.Length == 0 ? null : db, pages)); |
There was a problem hiding this comment.
Non-blocking edge case, on both SKUs (same line at Lite/Analysis/DuckDbFactCollector.QueryPerf.cs:121).
A NULL database_name or query_hash folds to string.Empty and the name is still built as db + "/" + hash, so the operator-facing string can come out as `Sales/`, `/0xAB` or — when both are NULL — `/`. That string is not internal: it becomes Fact.ObjectName, which FactAdvice.NamedQueriesSentence prints verbatim ("Most spilled: Sales/ (500 pages (3.91 MB))") and FactIdentity.Describe wraps as "most spilled query /".
This is reachable rather than theoretical — the aggregate's own comment at line 50 says "A NULL hash collapses to one per database, as for spilling_queries", so the read is written in the knowledge that NULL hashes occur, and the COALESCE(v.database_name, '') at line 53 says the same for the database. Keeping the identity equal to the aggregate's is the right call; the question is only what the card prints. Falling back to a legible placeholder (or declining to name an entry whose hash is NULL, since an unnamed query is not a name an operator can act on) would keep the prose honest. Entirely your call — the counts are unaffected either way.
| Action<System.Collections.Generic.List<Fact>> assert) | ||
| { | ||
| var connectionString = Environment.GetEnvironmentVariable("DARLING_TEST_PG"); | ||
| Assert.SkipWhen(string.IsNullOrEmpty(connectionString), "Set DARLING_TEST_PG to a Postgres connection string to run the #6640 live tests."); |
There was a problem hiding this comment.
Non-blocking, flagging the coverage shape rather than asking for a change.
Both tests in this class skip unless DARLING_TEST_PG is set, so on an ordinary CI run the Darling half of this PR — the three new columns in ParameterSensitivitySql, the new tie-break, the QuerySpillsRankedSql read, the ordinal reads at lines 308-310 and 120-122 of PgFactCollector.QueryPerf.cs — has no behaviour test that executes. Lite's FactsNameQueriesTests covers the twin logic, but not Darling's own SQL: the two statements differ (FROM latest, svr vs FROM latest, make_interval vs the de-skewed creation_time_utc, Convert.ToDouble vs ToInt64), and the ordinals are read positionally against each SELECT list separately. A wrong ordinal or a column dropped from the Postgres SELECT list would be green here and green in Lite.
There is also no Darling twin of Lite's ParameterSensitivity_TiedRatios_AreNamedInKeyOrder, even though the tie-break landed on both reads — so the Postgres ORDER BY worker_ratio DESC, database_name, query_hash, query_plan_hash is the one half of that fix with nothing exercising it.
If the live store is reachable in whatever runs these, a tie case here would close the gap; otherwise it is worth a line in the body saying the Darling collector half rests on the live run.
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
Claims
- VERIFIED — PARAMETER_SENSITIVITY sets
ObjectName/Ranked, the read returns the three name columns,Valueis the worker ratio,Figurescarries the four numbers —PgFactCollector.QueryPerf.cs:243-245,306-337,DuckDbFactCollector.QueryPerf.cs:260-262,316-345;ranked[0]andworstRatioboth come from the first kept row. - VERIFIED — "Existing Metadata keys are unchanged" — both hunks insert above an untouched
Metadatainitializer (PgFactCollector.QueryPerf.cs:336-347). - VERIFIED — the second statement runs on the same connection, top three, only when the aggregate found spills —
PgFactCollector.QueryPerf.cs:110-125,DuckDbFactCollector.QueryPerf.cs:107-123. - VERIFIED — "
ranked[0].valueis at mostvalue" — the total sums every in-window row, the ranked read one group'sdelta_spills > 0rows, andCollectorDeltaCalculator.cs:572-598clamps a reset to 0 rather than a negative delta. - VERIFIED —
AttachSubjectaddsobject_namefor these two keys only, every other fact keeps its bytes, both tools call it —FactRankedPayload.cs:88-89,McpAnalysisTools.cs:363,DarlingMcpTools.cs:429; pinned byFactPayload_CarriesObjectName_ForTheTwoQueryFactsOnly(Assert.Same). - VERIFIED —
Attachstill appendsrankedlast over a JsonObject —FactRankedPayload.cs:109-112, asserted by output inFactPayload_AttachSubjectThenAttach_KeepsRankedLast_AndTheSubjectOnTheWire. - VERIFIED — the card text names the queries for both facts —
FactAdvice.cs:1640,:1726viaNamedQueriesSentence(:1661-1666), which returns empty when nothing ranked, so a store replay degrades instead of throwing. - VERIFIED — both SKUs end
ORDER BY worker_ratio DESC, database_name, query_hash, query_plan_hash—PgFactCollector.QueryPerf.cs:253,DuckDbFactCollector.QueryPerf.cs:270;ParameterSensitivity_TiedRatios_AreNamedInKeyOrderis the Lite behaviour test. - VERIFIED — the HUGEINT fix, and Darling never affected —
DuckDbFactCollector.QueryPerf.cs:120uses the BigInteger-aware helper atBlockingPairRowQuery.cs:94-99; Darling keepsConvert.ToDoubleatPgFactCollector.QueryPerf.cs:122, correct for Npgsql numeric. - VERIFIED —
ParameterSensitivityFiringSignatureParityTestsdoes not match the new SQL — its regex (Lite.Tests/ParameterSensitivityFiringSignatureParityTests.cs:103-110) ends at the ratio predicate and never reaches the SELECT list orORDER BY; the predicate block is untouched on both SKUs. - UNVERIFIED — the seeded timings (158 ms Postgres, 3-5 ms DuckDB) and the two production timings — live measurements a read-only tree cannot reach.
- UNVERIFIED — "Logic ran locally against Postgres 16 in a scratch harness (both passed)" —
FactsNameQueriesLiveTests.cs:88skips unlessDARLING_TEST_PGis set; needs a run with it pointed at a live store. - UNVERIFIED — "Lite
FactsNameQueriesTests… Builds locally" — needs a Windows build and test run. - UNVERIFIED — "
Darling.TestsandLite.Testsbuild to 0 errors" — needs a build. - UNVERIFIED — "The literal revert runs in CI on #6643 … Each test above should fail there" — needs that run.
- REFUTED — the comment this diff writes at
FactIdentity.cs:72-75, that the newly added keys are rostered "not because an edge reaches them today". Six edges reach them:RelationshipGraph.cs:198,:208,:403,:440intoQUERY_SPILLS;:168,:223intoPARAMETER_SENSITIVITY. Consequence in the inline comment.
Riskiest lines
PerformanceMonitor.Analysis/FactIdentity.cs:83-84— the only change that alters findings this PR is not about:InferenceEngine.cs:356intoFactAdvice.cs:355appends aBehind it: …sentence to the ROOT card of any story consuming either query fact as a hop, and that text is frozen into the persistedStoryTextthe viewer, the alert e-mail and both MCP tools render. UNCOVERED — nothing builds such a story and asserts the root's investigation;StoryNamedHopsTestsexercisesDescribe/Sentencein isolation and cannot fail on a wrong roster decision.Darling/PerformanceMonitor.Darling.Analysis/PgFactCollector.QueryPerf.cs:308-310— three new positional ordinals against a SELECT list extended in the same hunk; an off-by-one ships the plan hash as the database on every parameter-sensitivity card. UNCOVERED on this SKU —FactsNameQueriesLiveTests.ParameterSensitivity_NamesTheOffenders_WorstFirst_WithinTheCapfails on it but skips withoutDARLING_TEST_PG; Lite's twin pins Lite's SELECT list, not this one.Lite/Analysis/DuckDbFactCollector.QueryPerf.cs:109-123— reader closed mid-method, a second command on the same DuckDB connection under the held read lock, then a HUGEINT conversion; this already cost every QUERY_STATS fact once (run 39981348396) because the block sits inside the catch that degrades to "no facts", so a throw here takes QUERY_HIGH_DOP with it. Covered byFactsNameQueriesTests.QuerySpills_NamesTheSpillingQueries_MostSpillsFirst_WithinTheCap.
Traps near this diff
- "Lite and Darling ship in parity … a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit" — does not apply: both readers (
DuckDbFactCollector.QueryPerf.cs:18,PgFactCollector.QueryPerf.cs:68), both tools' descriptions (McpAnalysisTools.cs:762,771,DarlingMcpTools.cs:411,420), shared advice (FactAdvice.cs:1661). - "Ruling (#5833): tests are load-bearing … never answer a trap with a test that reads source text" — applies to the edited pin only:
FactRankedTests.cs:154-155is updated, not added. All three new files assert behaviour;FactsNameQueriesAdviceTests.cs:78-87re-checks the tool wiring by output. - "A count taken off the page and named as the population's" — does not apply:
spilling_query_countcomes from theCOUNT(DISTINCT …)above theLIMIT(PgFactCollector.QueryPerf.cs:54-55),offender_countfrom the full scan (:323). - "Truncation inferred from a full page" — does not apply: no truncation flag is derived from the ranked list; the cap is stated as "capped at three" in both descriptions.
- "
ORDER BY 3 + 4 DESC… Sort by named columns" — does not apply, and the diff moves toward it: both new orderings name their columns (PgFactCollector.QueryPerf.cs:80,:253). - "
FactAdvice.PopulateStoryTextruns BEFOREClusterIntoIncidents/StampClusters/Reconcile" — does not apply: both sentences are appended inside the composers (FactAdvice.cs:1640,:1726), before the blob is frozen. - "Two copies of one definition pinned EQUAL to each other … Alias, do not copy." — does not apply: one copy per SKU in two dialects, this repo's standing shape, and no test pins the texts equal.
- "A delta family that stores no interval cannot tell a restart's zero from a quiet interval" — does not apply: no new delta column;
delta_spillsis already clamped atCollectorDeltaCalculator.cs:596.
Divergence (blind read vs PR body)
One, and it sits in the diff rather than the body. The body treats the FactIdentity roster edit as a naming nicety; the comment at FactIdentity.cs:72-75 goes further and tells the next reader no edge reaches these keys today. The graph says otherwise in six places, so the edit changes the composed investigation text of root cards for MEMORY_GRANT_PENDING, RESOURCE_SEMAPHORE, TEMPDB_USAGE and CPU_SPIKE stories — persisted text on four surfaces. Neither the comment nor the body says so, and no test would fail if that turned out to be unwanted. Fix the sentence and say it in the body; everything else held up on every claim graded above.
Two smaller things the body omits, both non-blocking and both inline: PgFactCollector.AllSql's "every query this collector executes" no longer covers the new statement (it would pass every pin it faces, so this is forward-looking), and a NULL database_name or query_hash renders a name like Sales/ or / into the card prose.
Reviewer checklist
- Run the Darling halves with
DARLING_TEST_PGset:FactsNameQueriesLiveTestsis the only thing executing the new Postgres SELECT list, its ordinals and the tie-break. - On a live SQL Server with grant pressure, call
analyze_serverand read what theMEMORY_GRANT_PENDING/TEMPDB_USAGEroot cards now say — whether the newBehind it:sentence reads well there is a judgement only a real card settles. - Confirm the #6643 revert run actually fails the three new test classes.
- Re-time the ranked read on a store with thousands of distinct spilling queries: the
GROUP BYruns before theLIMIT 3, and the production timings are from the servers with the most spills, not the most distinct hashes.
…l read (#6640) Answers the review on #6641. The roster keeps QUERY_SPILLS and PARAMETER_SENSITIVITY: the graph reaches both as hops, so root cards of grant, tempdb and CPU stories now name the query, which is what #6640 asks for. The FactIdentity comment now says so. One behavioural test builds a MEMORY_GRANT_PENDING story with a QUERY_SPILLS hop through the real engine. A NULL database or hash now reads as a placeholder through one shared FactRanked.QueryName in both SKUs. The ranked spill read has its own try in both collectors, so a throw loses only the names. QuerySpillsRankedSql joins PgFactCollector.AllSql and the count pin moves to +5.
|
Claude posting for Erik Darling. Pushed afe2d7d (merged current dev). Draft state unchanged. Nothing was run locally beyond the two builds (0 errors each), because the test hosts here lack the Windows desktop runtime. CI will run the tests. REFUTED comment at FactIdentity.cs:72-75: the reviewer is right and the comment was wrong. Both keys are hops today. The roster entries stay, because naming the query on the root card of a grant, tempdb or CPU story is what #6640 asks for (the agent in the report could not find the query from the story). The comment now names the edges. The PR body says root cards gain the sentence. New test NULL names (inline): one shared AllSql comment (inline): added PG ordinals (inline): re-read 8, 9 and 10 against the SELECT list ( DuckDB block (riskiest line 3): it could cost the other QUERY_STATS facts, since it sat inside the method's catch. The ranked read now has its own try in both SKUs. A throw clears the names and logs without marking the family unread. Live-test coverage shape (inline): acknowledged. The Darling collector half rests on the live run. No Darling tie test was added. |
|
Blind-read note (non-blocking), on one claim in the body: "The That is true of the doc the claim names —
Both are prose, nothing executes on them, so this is a comment and not a verdict. But #6640's whole mechanism is "a reader finds the name where the doc says it is", and these two are exactly where a later lane would go looking for "does any SQL Server family rank?". Two smaller observations I checked and am NOT raising as findings, recorded so the next reviewer does not re-derive them:
|
There was a problem hiding this comment.
Verdict: LGTM
Claims
- VERIFIED — PARAMETER_SENSITIVITY sets
ObjectName/Rankedoff a read extended withdatabase_name,query_hash,query_plan_hash; ordinals 8/9/10 are the last three of each SELECT list andValue/Metadata are untouched —Lite/Analysis/DuckDbFactCollector.QueryPerf.cs:270-272,326-341,354;Darling/PerformanceMonitor.Darling.Analysis/PgFactCollector.QueryPerf.cs:252-254,315-333,347. - VERIFIED — QUERY_SPILLS ranks
(database, query_hash)by pages, top three, on a second statement on the same connection, only when the aggregate found spills, each in its owntrywithrecord: false—DuckDbFactCollector.QueryPerf.cs:18-31,106-133;PgFactCollector.QueryPerf.cs:68-81,109-134. The Lite CI fix reads the HUGEINT sum withToInt64(:124, unwrapped atBlockingPairRowQuery.cs:94-99). - VERIFIED —
Valuestays the window total andranked[0].valueis at mostvalue:delta_spillscannot go negative (CollectorDeltaCalculator.cs:570-575reports a reset as 0), so per-query sums overdelta_spills > 0cannot exceedSUM(delta_spills). Pinned byFactsNameQueriesTests.QuerySpills_NamesTheSpillingQueries_MostSpillsFirst_WithinTheCap. - VERIFIED — both cards'
Investigationnames the queries —FactAdvice.cs:1640,1726viaNamedQueriesSentence(:1661-1666), which unlikeNameTheRestListincludes entry [0]; bothFactsNameQueriesAdviceTestsadvice tests assert the wording. - VERIFIED —
AttachSubjectaddsobject_namefor these two keys only, every other fact gets its own reference back, both tools call it,rankedstays last, and no migration rung exists —FactRankedPayload.cs:77-95;McpAnalysisTools.cs:362,388;DarlingMcpTools.cs:469,478;InferenceEngine.cs:383. Pinned by bothFactPayload_*tests. - VERIFIED — the two keys joined
FactIdentity.SqlServerKeyswithDescribearms and both are reachable hops, so a root card gains the "Behind it" sentence —FactIdentity.cs:87-88,163-166;RelationshipGraph.cs:168,198,208,223,403,440;StoryNamedHopsTests.MemoryGrantPendingRootConsumingQuerySpills_NamesTheSpillingQueryOnTheRootCardruns the real engine and graph. Both SKUs' tiebreaks landed (DuckDbFactCollector.QueryPerf.cs:280,PgFactCollector.QueryPerf.cs:262) without disturbingParameterSensitivityFiringSignatureParityTests' canonical block. - UNVERIFIED — every timing in the body (158 ms vs a 285-317 ms aggregate on PG 16, 3-5 ms on DuckDB, and the two production-store pairs) — live measurements a read-only tree cannot reproduce.
- UNVERIFIED — "Logic ran locally against Postgres 16 in a scratch harness (both passed)" and "builds to 0 errors" —
FactsNameQueriesLiveTestsskips unlessDARLING_TEST_PGis set (:199-200), and a build is not something this tree can run. - UNVERIFIED — the #6643 revert proofs, in particular the tiebreak one: without the tiebreak DuckDB's order among equal
worker_ratiorows is unspecified rather than guaranteed insert order, so whetherParameterSensitivity_TiedRatios_AreNamedInKeyOrderfails on the revert is empirical, not shown by the source.
Riskiest lines
Darling/PerformanceMonitor.Darling.Analysis/PgFactCollector.QueryPerf.cs:319-321— three new positional ordinals; an off-by-one names the wrong query or throws on every Darling pass. Covered byFactsNameQueriesLiveTests.ParameterSensitivity_NamesTheOffenders_WorstFirst_WithinTheCap, which skips withoutDARLING_TEST_PG— UNCOVERED in a run with no live Postgres.Lite/Analysis/DuckDbFactCollector.QueryPerf.cs:109— closing the aggregate reader to run a second command on the same DuckDB connection inside the held read lock; a throw here cost QUERY_SPILLS and QUERY_HIGH_DOP in run 39981348396. Covered byFactsNameQueriesTests.QuerySpills_NamesTheSpillingQueries_MostSpillsFirst_WithinTheCap, which asserts the ranked list and the aggregate metadata together.PerformanceMonitor.Analysis/FactRankedPayload.cs:88-93— the key gate plus re-serialize-and-append; a widened predicate changesget_analysis_factsbytes for every fact in the product. Covered byFactPayload_CarriesObjectName_ForTheTwoQueryFactsOnly(its reference-identity arm fails as soon as another key serializes).
Traps near this diff
- "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted (#3684). Alias, do not copy." — does not apply. The two
QuerySpillsRankedSqlbodies are per-SKU dialect copies pinned equal by no test, and the part that must not drift is aliased intoFactRanked.QueryName(AnalysisModels.cs:908), called from both (DuckDbFactCollector.QueryPerf.cs:125,PgFactCollector.QueryPerf.cs:126). - "Lite and Darling ship in parity … a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit." — does not apply. Collectors, both tool descriptions (
McpAnalysisTools.cs:850,859/DarlingMcpTools.cs:450,459) and bothAttachSubjectcall sites are paired. - "A count taken off the page and named as the population's … the page's count is
*_returned." — does not apply.spilling_query_countstill comes from the un-LIMITed aggregate (DuckDbFactCollector.QueryPerf.cs:149), so the card says "4 distinct queries" while naming three. - "
InferenceEngine.BuildStoriesconsumes facts at or above 0.5 as their own roots BEFORE lower-severity roots walk." — does not apply, and the new story test respects it:StoryNamedHopsTestsholds QUERY_SPILLS at 0.40 so the traversal keeps it as a hop. - "Never add one, and never answer a trap with a test that reads source text." — applies to the surrounding censuses, not to this diff's new tests: the two source-reading tests touched (
FactRankedTests.cs:154-155,PgFactCollectorTests.cs:119-120) are pre-existing pins kept green, and every new test here is a behaviour test.
Divergence (blind read vs PR body)
- "The
Fact.Rankeddoc no longer says no SQL Server collector sets it." That doc was updated (AnalysisModels.cs:84-95), but two other copies of the sentence survive and now state the opposite of what the tree does:FactAdvice.cs:69andDarling/Darling.Tests/FactRankedTests.cs:102-104. Prose only; posted as a comment. - "Considered, not added" says an
AssertInvariantcall on the two new facts "would fail it by design". True for QUERY_SPILLS (FactRankedTests.cs:52assertsValue == Ranked[0].Value), but PARAMETER_SENSITIVITY satisfies the invariant in full —ValueisworstRatioandranked[0].Valueis the same ordinal-2 read on the same first kept row. The shared helper is now the one place that does not know about the documented exception. Deliberate per the body, so it stays a note. - Unstated rather than contradicted: when the ranked read throws,
spillRankedis cleared andObjectNamestays null (DuckDbFactCollector.QueryPerf.cs:130,143), soAttachSubjectfalls through to the reference-identity arm and the card is byte-identical to the pre-#6640 card.
Reviewer checklist
- Run
FactsNameQueriesLiveTestswithDARLING_TEST_PGagainst a real Postgres 16 store — the only thing exercising Darling's ordinals 8-10 and theConvert.ToDoubleover whatever Npgsql returns forSUM(delta_spills). - Confirm
Lite.Testspasses on the Windows desktop runtime;FactsNameQueriesTestsis the only behaviour coverage for the Lite collector changes. - Check the #6643 revert run turns
ParameterSensitivity_TiedRatios_AreNamedInKeyOrderred — without the tiebreak DuckDB's order among equal ratios is unspecified, so it could pass by luck. - Read one real QUERY_SPILLS card on a busy server:
Most spilled: Sales/0xAB (1,234 pages (9.6 MB)), …nests a parenthetical inside a parenthetical. - Watch one analysis pass's wall clock on the largest production store — the ranked read is a second full group-by over the window's
query_statsrows and fires on every pass where anything spilled.
|
Claude posting for Erik Darling. CI at ff932e1 failed one test per SKU, and 2e8c923 fixes both.
|
| } | ||
|
|
||
| /// <summary>#6640: " Label: name (figure), name (figure)." for the queries a fact ranks, subject first, or empty when the | ||
| /// fact ranked none. Unlike <see cref="NameTheRestList"/> it includes entry [0], because these cards have no other sentence |
There was a problem hiding this comment.
Non-blocking, doc drift in the file this hunk lands in. NamedQueriesSentence is added 13 lines below two statements that this change makes false, and neither was updated:
- line 68: "every fact that ranks nothing (nearly all of them, and every SQL Server fact — no SQL Server collector sets
Fact.Ranked)" — two SQL Server collectors now do (DuckDbFactCollector.QueryPerf.cs:144/:355,PgFactCollector.QueryPerf.cs:145/:348). - lines 81–82: "Today's three callers are all PostgreSQL-target." —
NamedQueriesSentenceis a fourth reader of the seam and both its callers are SQL Server families.
The PR body says "The Fact.Ranked doc no longer says no SQL Server collector sets it", and that is true of AnalysisModels.cs:88 — but the same sentence survives here and in Darling/Darling.Tests/FactRankedTests.cs:102 ("every SQL Server fact", on the byte-identity test's summary). Three copies of one claim, two of them now wrong, is the shape .github/REVIEW-TRAPS.md names under "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted" — here they are prose rather than pins, so nothing goes red; it just misleads the next reader of this exact seam.
There was a problem hiding this comment.
Verdict: LGTM
Claims
- VERIFIED - PARAMETER_SENSITIVITY sets ObjectName/Ranked, name database/query_hash/query_plan_hash, Value the worker ratio, Figures the four keys, Metadata unchanged; the three new columns end each SELECT list and the reader takes them at ordinals 8/9/10 - DuckDbFactCollector.QueryPerf.cs:270 vs :328; PgFactCollector.QueryPerf.cs:252 vs :319.
- VERIFIED - QUERY_SPILLS runs a second statement on the same connection, grouped (database_name, query_hash), ordered by pages, capped at FactRanked.MaxObjects, only when the aggregate found spills - DuckDbFactCollector.QueryPerf.cs:18/:107; PgFactCollector.QueryPerf.cs:68/:110.
- VERIFIED - Value stays the window total and ranked[0].value is at most value (:141; delta_spills cannot go negative, QueryStatsCollector.cs:729-760), pinned by FactsNameQueriesTests.QuerySpills_NamesTheSpillingQueries_MostSpillsFirst_WithinTheCap.
- VERIFIED - aggregate figures untouched: spilling_query_count (PgFactCollector.QueryPerf.cs:54) and offender_count (:351) are still population counts, never the capped list's length.
- VERIFIED - the card text names the queries for both facts (FactAdvice.cs:1640, :1726 via :1661; FactsNameQueriesAdviceTests), including entry [0], which is what covers the one-query case where analyze_server's root_fact carries no name.
- VERIFIED - AttachSubject adds object_name for these two keys only, other facts keep their bytes by reference return, both tool files call it, ranked still lands last - FactRankedPayload.cs:77-95; McpAnalysisTools.cs:362; DarlingMcpTools.cs:486; FactPayload_CarriesObjectName_ForTheTwoQueryFactsOnly.
- VERIFIED - Lite has the same change, no migration rung (AnalysisModels.cs:88); the invariant exception is stated at AnalysisModels.cs:11-15 and in both get_analysis_facts descriptions, and the SKUs' four tool descriptions are byte-identical.
- VERIFIED - the CI fix reads DuckDB's SUM(BIGINT) through the BigInteger-aware ToInt64 (DuckDbFactCollector.QueryPerf.cs:124, BlockingPairRowQuery.cs:94); Darling keeps Convert.ToDouble over Npgsql's numeric (PgFactCollector.QueryPerf.cs:125), correct for that provider.
- VERIFIED - FactRankedTests.BothToolFiles_ updated to the AttachSubject shape with the tool files unchanged (:150); the four other source pins the body names do not match the new SQL or code, and ArchiveTieOrderTests reads only offender_count/worst_ratio.
- VERIFIED - both facts are hops of the named roots today (RelationshipGraph.cs:168, 198, 208, 223, 403, 440); roster and Describe arms at FactIdentity.cs:87/:163, roster test updated at StoryNamedHopsTests.cs:36. root_fact still carries no object_name on analyze_server (McpAnalysisTools.cs:217), as declined.
- VERIFIED - both SKUs end ORDER BY worker_ratio DESC, database_name, query_hash, query_plan_hash (:280; :262), pinned by ParameterSensitivity_TiedRatios_AreNamedInKeyOrder.
- VERIFIED - the census bumps land on real counts: 30 classified catches in PgFactCollector., 31 in DuckDbFactCollector., AllSql 37 = 32 + 5, and the new SQL clears the AllSql dialect pins (PgFactCollectorTests.cs:222).
- UNVERIFIED - the seeded timings and seed shape (1M rows, 20k queries, ~14% spilling) - a live measurement.
- UNVERIFIED - the two production timings - measured on named stores; nothing in the repo records them.
- UNVERIFIED - "logic ran locally against Postgres 16 in a scratch harness" - FactsNameQueriesLiveTests is Assert.SkipWhen on DARLING_TEST_PG (:217) and only nightly.yml sets it.
- UNVERIFIED - "Darling.Tests and Lite.Tests build to 0 errors" - dotnet build was unavailable in this sandbox, so every compile-level claim rests on CI.
- UNVERIFIED - the #6643 revert run - promised, not yet in the body.
- UNVERIFIED - the CI-fix root-cause narrative (run 39981348396, the deleted repro console) - the tree's fix matches the mechanism; the history is not checkable.
Riskiest lines
- PgFactCollector.QueryPerf.cs:319 - three columns read by positional ordinal; an off-by-one names the wrong query in object_name, ranked and the new "Behind it:" sentence, with no exception to notice - UNCOVERED on PR CI (FactsNameQueriesLiveTests.ParameterSensitivity_NamesTheOffenders_WorstFirst_WithinTheCap skips unless DARLING_TEST_PG is set; only nightly.yml sets it).
- DuckDbFactCollector.QueryPerf.cs:109 - reader.CloseAsync() now sits above both totalSpills blocks and above QUERY_HIGH_DOP; a read off reader past it throws into the outer catch and costs the whole family - covered by QueryHighDopStaleMaxDopTests (same SELECT, :101) with QuerySpills_NamesTheSpillingQueries_MostSpillsFirst_WithinTheCap.
- DuckDbFactCollector.QueryPerf.cs:124 - the HUGEINT read that already cost every QUERY_STATS fact once - covered by QuerySpills_NamesTheSpillingQueries_MostSpillsFirst_WithinTheCap, which runs it against a real DuckDB.
Traps near this diff
- "a bare integer in ORDER BY is an output-column ordinal, but 3 + 4 is an expression ... Sort by named columns." - does not apply - both new sorts name columns (PgFactCollector.QueryPerf.cs:80, :262).
- "A percent whose denominator is the PAGE ... Ask what a share is of." - does not apply - no share; each entry carries its own absolute SUM(delta_spills) (:72) and the total stays the window aggregate (:148).
- "A count taken off the page and named as the population's." - does not apply - :54 and :351 are population counts, not the capped list's length.
- "Truncation inferred from a full page ... the flag is fetched.Count > limit." - does not apply - nothing infers truncation; the cap is declared (AnalysisModels.cs:46).
- "PopulateStoryText runs BEFORE ClusterIntoIncidents / StampClusters / Reconcile, so a sentence added after folding must re-read the frozen StoryText." - does not apply - the prose is composed inside the two composers (FactAdvice.cs:1640, :1726) and the hop sentence rides the existing path (FactIdentity.cs:211).
- "BuildStories consumes facts at or above 0.5 as their own roots BEFORE lower-severity roots walk." - applies, honoured - QUERY_SPILLS is seeded at 0.4 (StoryNamedHopsTests.cs:231).
- "A key in the graph is not a key in the data; grep the collector that emits it." - applies to the roster comment's edge list, checks out - every edge named exists and both keys are emitted by both SKUs.
- "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted. Alias, do not copy." - applies weakly - two engines, two statements; the shareable piece is shared (FactRanked.QueryName, AnalysisModels.cs:56). The prose half is my inline comment.
- "Lite and Darling ship in parity: a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit." - applies, satisfied - both collectors, both tool files, four identical descriptions.
- "never answer a trap with a test that reads source text." - applies, respected - the new coverage is behaviour; the source pins edited are pre-existing censuses kept in step.
- "InternalsVisibleTo on PerformanceMonitor.Analysis names Lite.Tests only - a helper Darling.Tests must call has to be public." - applies, satisfied - AttachSubject and QueryName are both public.
Divergence (blind read vs PR body)
One, narrow. The body says "The Fact.Ranked doc no longer says no SQL Server collector sets it." True of AnalysisModels.cs:88, but the same claim survives at FactAdvice.cs:68, thirteen lines above the new NamedQueriesSentence, at FactAdvice.cs:81-82 ("Today's three callers are all PostgreSQL-target"), and at Darling/Darling.Tests/FactRankedTests.cs:102. Prose only, nothing goes red; left as the one non-blocking inline comment. Otherwise the accounts agree, and twice the body is ahead of a blind read: "Considered, not added" pre-empts the AssertInvariant gap I reached independently, and "Root cards gain the query name" discloses the roster's effect on frozen StoryText, which "What changed" alone would not have led a reader to.
Reviewer checklist
- Run the Darling suite with DARLING_TEST_PG set and confirm FactsNameQueriesLiveTests executes rather than skips; PR CI sets it nowhere, so riskiest line 1 lands unexecuted.
- Build and run Darling.Tests + Lite.Tests; dotnet build was unavailable here.
- Post the #6643 revert-run results; until then the revert proof for all seven new tests is a claim about a run nobody can read.
- Re-time the ranked read on a store with thousands of spilling queries: the bound is the GROUP BY over the window, not the LIMIT 3 the body reasons from.
- Read one real QUERY_SPILLS and one PARAMETER_SENSITIVITY card in the viewer and in an e-mail body: the sentence nests parentheses, and the label reads "Worst plans" even when one plan rode.
|
Claude posting for Erik Darling. Revert proof, recorded after the merge. #6643 undid this PR's code on dev at c114110 and kept its tests: the Darling and Lite collectors set no query names on PARAMETER_SENSITIVITY and QUERY_SPILLS, the card text names no queries, and the fact payload adds no object_name. CI run 40132354137 at 1b6162b failed 9 of the 11 new tests:
Two new tests passed, as expected, because the revert leaves their code in place:
No other test failed. Darling.Tests failed only these 6, and Lite.Tests failed only these 3. #6643 is now closed and its branch deleted. |
Claude posting for Erik Darling.
Closes #6640. The base is not the default branch, so this will not auto-close it.
What changed
ObjectNameandRanked. The read returnsdatabase_name,query_hashandquery_plan_hash. The name isdatabase/query_hash/query_plan_hash. The plan hash sits in the name becauseFiguresholds numbers only.Valueis the worker ratio.Figurescarriesmin_worker_us,max_worker_us,grant_ratioandspill_divergence. Existing Metadata keys are unchanged.ObjectNameandRanked. A second small statement on the same connection ranks (database, query_hash) by spilled pages, top three. It only runs when the aggregate found spills. The name isdatabase/query_hash, and each ranked value is that query's own spilled pages. The fact'sValuestays the window total, soranked[0].valueis at mostvalue. This is the one stated exception to theRankedObjectinvariant. The aggregate figures and thresholds are untouched.Investigation) lists the named queries for both facts, so the finding text an agent reads names them.get_analysis_factsshowedrankedonly at two or more entries, and never showedobject_name. A newFactRanked.AttachSubjectaddsobject_namefor these two keys only. Every other fact keeps its bytes. Both MCP tools (Darling and Lite) call it.Timing
Seed: one million query_stats rows in a 4 hour window, 20k distinct queries, about 14% with spills. The new statement was about 158 ms on Postgres 16 (cold 157, warm 159) and 3 to 5 ms on DuckDB. The existing aggregate was 285 to 317 ms on Postgres and about 4 ms on DuckDB, and it is unchanged. So the new read adds roughly half the aggregate cost and only when spills exist.
Measured on two production stores, on the server with the most spills in each over the last 4 hours. The new ranked read took 37.6 ms on the first run and 25.4 ms on the second on one store, and 36.8 ms and 27.0 ms on the other. The existing aggregate took 36.3 ms and 30.8 ms on the same servers. The new read runs only when the aggregate found spills.
Tests and revert proof
FactsNameQueriesLiveTests(both)ObjectNameis null andRankedempty, so the name asserts failFactsNameQueriesAdviceTests(3)AttachSubject, the sentence andobject_nameare missingFactsNameQueriesTests(2)The literal revert runs in CI on #6643, which is a throwaway PR. It stops the Darling and Lite collectors naming the queries, stops the card text naming them, and stops the payload adding object_name. Each test above should fail there. The results go here when that run ends. The full suites also wait for CI.
CHANGELOG entry
get_analysis_facts(object_name,ranked). Darling and Lite. (Analysis: PARAMETER_SENSITIVITY and QUERY_SPILLS facts name the queries behind them #6640)Considered, not added
object_nameon facts other than these two: it would change other payload bytes.plan_hashfield:RankedObjecthas no slot for a string, so the plan hash is in the name.CI fix (run 39981348396)
Root cause (Lite, 12 failures).
QuerySpillsRankedSqlselectsSUM(v.delta_spills). DuckDB typesSUM(BIGINT)as HUGEINT, and DuckDB.NET returns that asSystem.Numerics.BigInteger.Convert.ToDoublethrowsInvalidCastExceptionon aBigInteger.CollectQueryStatsFactsAsynccaught the exception, reported a collection failure and returned. So every QUERY_STATS fact was lost: QUERY_SPILLS, and QUERY_HIGH_DOP, which is built from the same read. The reader-close and second-command pattern was fine.Fix. The ranked read now uses
ToInt64(...)(the BigInteger-aware helper the aggregate already uses). The aggregate figures and thresholds are unchanged. The Darling side was never affected: Npgsql returnsSUM(bigint)asnumeric/decimal, whichConvert.ToDoublehandles.Reproduction. I ran a throwaway net10.0 console on macOS against DuckDB.NET 1.5.5 with the same view shape and parameter style. The ranked query itself ran fine, and the value came back as
BigInteger.Convert.ToDoublethrew the exactInvalidCastException.ToInt64returned the right sum. The console was deleted and none of it is committed.Darling
FactRankedTests.BothToolFiles_.... It pins the literal call shapeFactRanked.Attach(new. The tool files now callFactRanked.Attach(FactRanked.AttachSubject(new { ... }, f, opts), f.Ranked, opts).AttachSubjectreturns a JsonObject, soAttachstill appendsrankedlast. I updated the test's pattern and closing-tail assertion to that shape, with a comment. I kept the tool files as they are, so theget_analysis_factsbytes for other facts stay identical.Other pins checked. The other source-text tests that read the touched files (
ParameterSensitivityFiringSignatureParityTests,CreationTimeClockFrameDisciplineTests,QueryHighDopStaleMaxDopParityTests,QueryStoreSliceTieBreakSourceTests,ServerClockDstReadTests,McpToolAuditFactAdviceTextTests) do not match the new SQL or code.Darling.TestsandLite.Testsbuild to 0 errors. They cannot run on macOS, so CI is the arbiter.Review round (review 5630610823)
Value(the scorer thresholds on it) and made the exception explicit. TheRankedObjectdoc and bothget_analysis_factsdescriptions now say so. The Lite test pinsranked[0].value < value. Needs Erik only if he would rather rank on a different quantity.FactRankedTests.BothToolFiles_...: real, already fixed in a5c9b5d (pattern updated to theAttachSubjectshape). The newFactPayload_AttachSubjectThenAttach_...test checks the tool wiring by output, not by spelling:object_nameandrankedboth ride,rankedlast.object_nameundocumented: real. Bothget_analysis_factsdescriptions now state it for the two facts. Theanalyze_serverdescriptions name the two newroot_fact.rankedcards. TheFact.Rankeddoc no longer says no SQL Server collector sets it.FactIdentityroster: real.QUERY_SPILLSandPARAMETER_SENSITIVITYjoinedSqlServerKeyswithDescribearms ("most spilled query...", "most sensitive plan..."), so a consumed hop names its query. The Lite roster test is updated.ORDER BY worker_ratio DESC, database_name, query_hash, query_plan_hash. New Lite testParameterSensitivity_TiedRatios_AreNamedInKeyOrder. Revert proof: drop the tiebreak and the tied plans come back in insert order, so the test fails (waits for CI, Lite.Tests is Windows-only).publicconstant: real, cosmetic. Fixed on both SKUs.root_factcarries noobject_nameonanalyze_server: not added. The card'sInvestigationand the newBehind it:sentence name the query, andget_analysis_factscarriesobject_name. Adding it toroot_factchanges every finding's bytes, which is a design choice for Erik. Recommendation: add it later if agents miss the name.Considered, not added
FactRankedTests.AssertInvariantcall on the two new facts: the QUERY_SPILLS exception would fail it by design.LIMIT 3after a group by. The production timings above are the measure I have.Root cards gain the query name (review of afe2d7d)
Both query facts are hops in today's graph (
MEMORY_GRANT_PENDING,RESOURCE_SEMAPHORE,TEMPDB_USAGEandPARAMETER_SENSITIVITYreachQUERY_SPILLS,CPU_SPIKEandMEMORY_GRANT_PENDINGreachPARAMETER_SENSITIVITY). Rostering them inFactIdentitymeans the root card of those stories now carries a "Behind it:" sentence naming the query, and that text is frozen intoStoryText. That is what #6640 asks for, so it stays. The roster comment now says so.