Repository navigation
Release 3.11.0 - #6481
Merged
Merged
Release 3.11.0#6481
Conversation
… on the workflows give way to it (#5947) (#5966) Fixes #5947. Guard job runs actionlint on changed workflow files; pure string pins on the workflows give way to it (#5947) - a pull request that changes a file under `.github/workflows/`, and every push to main or dev, now runs actionlint in the Guard tests job, so a broken expression, a `needs:` name that matches no job, or a bad action input fails at once and not when the workflow runs.
…fresh it depends on (#5956) (#5963) Fixes #5956. WebServerPageRangeTests: the past-end poll test no longer misses a refresh it depends on (#5956) - The test built a live six-hour range from three separate clock reads, so a tick between them made the range 6 h + 1 ms, which the page rounds up to 7 hours. The test now reads the clock once.
…lpers replace fixed sleeps, and Thread.Sleep is banned in test code (#5943) (#5965) Fixes #5943. Tests: the CI skip marker is counted apart from failures, two wait helpers replace fixed sleeps, and Thread.Sleep is banned in test code (#5943) - the test projects get `TestWait.WaitUntil` and an `AnchorClock` in place of fixed sleeps, `Thread.Sleep` is a build error in them, and a new script counts the deliberate "CI not run" marker apart from real failures.
…the journal page (#5940) (#5969) Fixes #5940. DuckDbInvestigationStoreTests: the report-links test no longer races the journal page (#5940) - The journal page test makes 150 store writes and timed out once on a loaded runner because another test class held the process-wide DuckDB write lock past the store's 5 second budget. The investigation store tests now give their store a 2 minute lock wait.
… interval length (#5815, #5882) (#5959) Fixes #5815. The 24-hour Query Store top read sizes its time floor to the server's interval length (#5815, #5882) - - **The 24-hour Query Store top queries read touches fewer blocks on servers with short intervals** ([#5815], [#5882]) - On a busy store the default 24-hour `get_query_store_top` read pulled yesterday's whole partition of Query Store rows and threw most of them away, up to 38 s on the largest store. The read now sizes its lower time bound to the longest interval the server has reported (3 hours at the default 60 minutes, instead of 26) and keeps the 26 hours whenever that length is unknown; the Queries grid in the Viewer reads the same way.
…) (#5967) Fixes #5955. PR CI builds the Darling and Lite test projects in parallel jobs (#5955) - the Darling and Lite shards waited for one job that built both test projects in turn (about 6 minutes); each now waits only for its own project's build job, so the Darling shards start about 2 minutes earlier and the Lite shards about 2.5 minutes earlier. A red build of either project still turns the required checks red.
…nd report a buffer that still will not parse (#5948) (#5968) Fixes #5948. Ring-buffer collectors escape stray ampersands before the XML cast, and report a buffer that still will not parse (#5948) - A deadlock on a table or index whose name held `&` made the deadlock, blocked-process, client-timeout, long-query and system-health collectors fail on every run with "Cannot insert the value NULL into column 'ring_buffer'" until the session was restarted. Both products now escape the stray `&` before reading the buffer, and if a buffer still cannot be read the collector reports its size and SQL Server's own parse error instead.
… for 72 hours, never a test the PR touches (#5942) (#5964) Fixes #5942. CI quarantine list: a test dev's latest push failed is skipped on PRs for 72 hours, never a test the PR touches (#5942) - a broken test on dev failed every unrelated pull request until a fix merged. A new `.github/ci-quarantine.txt` lists such a test with its dev run, time and issue; the Guard job and the shard runners leave it out only while the latest dev push run failed it and the pull request does not touch it. The file ships empty.
…become behaviour tests (#5950) (#5973) Fixes #5950. Count pins that fail correct changes: ladder, MCP roster and DI pins become behaviour tests (#5950) - the migration ladder pins become one sentinel table and one theory, the MCP roster tests keep only the no-new-site direction with a ReadsLatestSnapshot attribute replacing the test-side roster, and the source-text DI and host pins give way to tests that resolve the services from the real container.
…an, then delete the Reads traits (#5961) (#5974) Fixes #5961. Derive the Reads=Lite and Reads=Darling class sets from the source scan, then delete the Reads traits (#5961) - The `Reads=Lite` and `Reads=Darling` trait attributes, their tagging script and the trait-roster tests are gone; the runners now run `-class` lists that `ci-select.py reads-classes` scans from the test sources.
…eb dashboard and MCP (#5941) (#5972) Fixes #5941. Run all collectors now, or one collector now, for a server from the web dashboard and MCP (#5941) - On a server's Collection Health tab, an editor seat now has "Run all collectors now" (every enabled collector, once) and a "Run now" button on each collector (that one, even if disabled, without changing its schedule); the MCP tool `run_collectors_now` does the same. A second request is refused while one is running, and a request within 60 seconds of the last one is refused with the time the next is allowed.
…ntu, and fix the 7 findings (#5970) (#5975) Fixes #5970. Lint the workflow shell scripts with actionlint and shellcheck on ubuntu (#5970) - an ubuntu job runs actionlint with shellcheck on over `.github/workflows/` when a pull request changes a workflow file, and its result feeds the required Darling PostgreSQL and Lite result checks, so a quoting or unused-variable mistake in a `run:` block fails the pull request before it merges.
…5736) (#5976) Part of #5736 (the field measurement runs on 10-20; the issue stays open for the numbers). Client Timeouts: the attention-rate measurement for the field week (#5736) - Two read-only scripts, `Darling/tools/attention-rate-measure.sql` for the PostgreSQL store and `Lite/tools/attention-rate-measure.sql` for the DuckDB store, report each server's per-minute client attention rate over the minutes the collector actually ran (minutes in span, minutes with a run, longest collection gap, attentions, p50, p99, p99.9, max and the three busiest minutes) and the fleet spread of the per-server p99.9, so the Client Timeouts alert's default bars can be set from measured data.
…or.Common (#5978) (#5994) Fixes #5978. Share the tag tree rules between Darling and Lite in PerformanceMonitor.Common (#5978) - The tag name, colour, depth and cycle rules now live in PerformanceMonitor.Common so Darling and Lite use one copy, and Lite's separate nesting limit is gone; server tag behaviour is unchanged.
…it touches a constant, an enum or a registration file (#5951) (#6013) Fixes #5951. Test selection runs everything for a shared-project change only when it touches a constant, an enum or a registration file (#5951) - A pull request that changes `PerformanceMonitor.Common` or `PerformanceMonitor.PlanAnalysis` no longer runs the whole suite unless the diff touches a `const`, an `enum` or a short list of registration files; any other change there selects tests by the coverage map like a product change.
…5977) (#5996) Fixes #5977. Query tags: the store tables, grants, server removal and retention (#5977) - Darling's store gains the tables query tags need: tags, hand assignments by query hash or object, rules, sightings, the tag map and a version row, with grants, server-removal cleanup and retention of aged sightings. Nothing reads or writes them yet.
#6000) (#6014) Fixes #6000. Investigation briefs say how often the alert fired at this time of day (#6000) - An investigation brief now says, for each metric linked to the request, how many times it fired and how many times it was muted within 30 minutes either side of the first alert's time of day over the previous 7 days (not counting the 30 minutes before the alert, which are the same incident), with the dates fired and the dates muted. A metric with no history says zero.
…ames (#5999) (#6015) Fixes #5999. Investigation requests, briefs and webhook tokens carry both server names (#5999) - Investigation requests, their briefs and the webhook's investigation token now carry both the server's display name and its instance name, read from the server registry when served, so a rename shows on the next read. `server_name` keeps the name the request was opened with.
… row saw (#5980) (#6016) Fixes #5980. Query tags: collectors describe the query keys and caller fields each row saw (#5980) - The collectors that see a query hash, a procedure or a caller (active queries, long-query completions, query stats, Query Store, procedure stats, blocked process reports, DMV blocking snapshots, deadlocks, attention events and default trace events) can now describe the query keys and the application, login and host each row saw, so the tag map can be built from them in a later change. Every hash is keyed in one `0x` text, whichever collector saw it. Nothing is stored yet.
…tagging rules (#5982) (#6018) Fixes #5982. Query tags: Darling MCP tools to create, edit, delete, list and test tagging rules (#5982) - Darling serves `create_query_tag_rule`, `update_query_tag_rule` and `delete_query_tag_rule` (with `mcp.allowWrites` and a token) and `list_query_tag_rules` and `test_query_tag_rule`. A rule can be tested against the recent query sightings of one server or all, showing what it would tag and what a hand assignment or a higher rule already holds, before it is saved.
Light review tier: allow 100 turns instead of 30 (#5683) The light review tier stopped at 30 turns and ran out on larger diffs before posting a verdict, so the required check failed with nothing to answer. The light tier's max_turns goes from 30 to 100 and the two comments that quoted 30 now say 100. Only .github/workflows/claude-review.yml changes. Merged to dev on Erik's instruction (10-17 01:1xZ, relayed by pm-worker-helper) when the review account reached 98%.
Fixes #5997. Phase C store: the one migration, grants and retention (#5997) - The Darling store gains the tables and columns that later investigation work uses: evidence per request section (cleared with the brief at 90 days), daily deadlock signature counts (kept 30 days), signature merges, report family and queries-run fields, follow-up fields, and signing keys on notification settings and routes that the read-only viewer and MCP roles cannot read.
…e store (#5989) (#6021) Fixes #5989. Query tags in Lite: the DuckDB tables and the tag, assignment and rule store (#5989) - adds the DuckDB tables and a tag, assignment and rule store that match Darling's, kept across an archive reset and cleared when a server is removed. Nothing is shown in the UI yet.
#6022) Fixes #5984. Query tags: Darling query reads carry the tag and filter by it (#5984) - Darling's top queries, top procedures, Query Store, active queries and long-query completions reads now show each row's query tag and take an optional `tag` filter that matches the tag and every tag beneath it. An unknown tag name answers empty and says so.
…t query tags (#5981) (#6020) Fixes #5981. Query tags: Darling MCP tools to create, edit, delete, assign and list query tags (#5981) - Six new Darling MCP tools create, edit, delete, assign, unassign and list query tags; the five writes are served only with `mcp.allowWrites`. A query can be tagged by `query_hash` or by object, for one server or every server, and a tag that has assignments, rules or child tags asks for confirmation before it is deleted.
…he queries run (#6004) (#6030) Fixes #6004. Investigation reports carry a family, fix links, evidence basis and the queries run (#6004) - A report can name the first report of its family (checked to exist), link a commit or a release as the fix, label each evidence item verified or inferred (served as two lists, with older evidence shown as unlabelled), and record up to 20 statements it ran, masked like any statement text. Darling reports no longer carry an `evidence` key: the evidence is served as `evidence_verified` and `evidence_inferred` (and `evidence_unlabelled` for items written before the basis was required), and a submit whose evidence items lack a `basis` is refused as `invalid`.
Fixes #5985. Darling writes query tag sightings and builds the tag map each pass (#5985) - Darling now records which query hashes, objects and callers each collection saw, and keeps a tag map up to date from your tag rules and hand assignments: a changed rule retags everything already seen on the next pass, and new queries are tagged as they appear. Sightings are buffered in memory and written once a minute by the map step, a query hash seen in two databases is recorded per database so a database rule matches each one, and a restarted service rebuilds its map in one pass instead of re-reading all history. Store schema version 194 (an index swap on the sightings table).
…llector readiness (#6001) (#6029) Fixes #6001. Investigation evidence is frozen at the open (#6001) - A High CPU investigation now captures its top statements, active requests by host and program, Query Store state, jobs and waits from the data already collected, right after it opens, so an agent reads what the breach looked like and not what is left of it by the time it claims. The brief carries the evidence, masked at the configured data detail level.
… no waiting (#6446) (#6448) * get_wait_trend: answer empty, not not_collected, for a stored wait type with no waiting in the window (#6446) * Lite test: ask get_wait_trend for an ignored wait type to get not_collected (#6446) SOS_SCHEDULER_YIELD is a wait type the collector stores, so after #6446 a miss on it reads empty. SLEEP_TASK is on the ignored list, the one case that stays not_collected with the collected list hint.
…nt, per-server categories (#6391) (#6463) * Viewer Job History: pick an Azure SQL Database, say it has no SQL Agent, and list only the picked server's categories (#6391) * Job History: the filter handler skips controls that do not exist yet (#6391) The Server combo's initial selection fires it inside InitializeComponent, before the Category combo and the empty note exist; the WPF smoke crashed there.
…t once per 24 hours (#6467) * Retention: skip the promoted legacy row delete, prune query_store_text once per 24 hours Once a Query Store interval table is promoted, DropExpiredAsync drops the whole legacy table when its bound S reaches the cutoff. Row-deleting its expired rows until then is wasted work, so the daily purge now leaves them. An unpromoted store, or a legacy table with no readable bound, still row-deletes. The collect.query_store_text prune is a full heap scan per call. A gate shared by the scheduled passes now lets it run at most once per 24 hours. The cutoff and its margin are unchanged. purge_now still prunes every time. * Retention: compute the text cutoff inside the prune gate, next to its delete DarlingRetentionTests.MapAndTextPurges_UseTheUnorderedCap_WithoutTheAdaptiveRetry looks for the text prune's delete call within 500 characters of the line that computes its cutoff. The gate's lines had pushed the call past that window. The cutoff now sits inside the gate block, right before the call. Behavior is unchanged. --------- Co-authored-by: scratch <scratch@local>
…over can rewrite the test assemblies (#6477) Every test-map shard has failed since nightly 569. AltCover (Mono.Cecil) threw "Failed to resolve System.Windows.Threading.DispatcherPriority" while writing Lite.Tests.dll. StaTestThread.WaitForIdle had gained an optional DispatcherPriority parameter (#6343), and an optional parameter stores a WindowsBase constant in the method's metadata. Cecil cannot resolve WindowsBase on the runner. An overload carries no such constant. The map shard's instrument step now also prints the exception line on failure. Its last 15 lines held only the stack trace, which hid the cause.
…#6449) * Flag a truncated Extended Events ring buffer in collection health (#6441) SQL Server cuts a ring_buffer target's XML at about 4 MB and keeps the oldest events. The system_health, long-query, deadlock and blocked-process reads now record the cut as counts on the run, collection health bands the collector WARNING with a plain sentence, and the health-parser reads no longer call the window quiet while the newest run says the buffer is cut. * Read the newest run's note without a row cap in Lite (#6441) * Keep the capture witness the events view; truncation is a second unavailable rung (#6441) The truncation read sits in its own method after the type-scoped witness, so the witness test's slice no longer contains collection_log. The empty-ladder test is edited to the new design: two unavailable rungs, the dead-session one unchanged.
* Release 3.11.0: version, changelog entries, archive split * Add the v3.10.0 migration ladder fixture * README sync for 3.11.0: collector gates, agent dispatch docs, what ships * CHANGELOG 3.11.0: apply the truth-check rulings * CHANGELOG 3.11.0: correct statements a later change in the release made false * Docs sync for 3.11.0: agent actions sentence, edition table, MCP tool counts, stale collector and tool counts * CHANGELOG 3.11.0: truth-check round 2, fold or drop entries whose before-state never shipped * CHANGELOG: name SQL Server in the named-pipe host entry * CHANGELOG: fix a missing period in the Level 3 routes entry * Darling upgrade shapes: the current-release pin names 3.10.0, the last shipped release The legs built their U1-U3 base from 3.9.0, so they checked an upgrade from a release one behind the one users run. Release checklist 8a(a). * Docs: README sync for 3.11.0 (tool counts, V214 rollback and provisioning, tab lists, MCP tool table, resolutions, named-pipe hosts) * CHANGELOG: fold #6447 into the notification route fallback entry * CHANGELOG: splice #6443 and #6450 into 3.11.0 * Docs: shorten the sentences added in the late splice and README sync * Upgrade shapes U1: give the hand-named backup to the service when a 3.10.0 base leaves it 3.10.0 hardens only timestamped backup names, so on a 3.10.0 base the leg's precondition never held. In the field the file is already service-owned from an earlier start; the leg sets that state and checks it again. * CHANGELOG: final splice of #6448, #6451, #6461 and #6463 into 3.11.0 * CHANGELOG: splice #6449 into 3.11.0
erikdarlingdata
enabled auto-merge (squash)
October 22, 2026 15:04
erikdarlingdata
disabled auto-merge
October 22, 2026 15:14
erikdarlingdata
enabled auto-merge
October 22, 2026 15:14
…#5506) (#6484) The text join hashed the server's whole query_store_text table to label 30 rows, because the planner expected about 3M rows in ranked where there are 3 to 4 thousand. A scalar subquery on the table's primary key probes 30 times instead. Adds a test for two databases sharing a query_id.
* WEB: Memory Grants, Resource Semaphore, Plan Corrections and the Query Store clutter tiles say where their data starts (part of #4966) (#5144)
The web server page's Memory Grant Pressure, Resource Semaphore Pressure, Plan Corrections and Query Store clutter panels now say where their data starts, so a server with a short history no longer shows a 7-day range as if it were all quiet.
The server adds the note for get_memory_grants and get_resource_semaphore (both probe memory_grant_stats, the table their window aggregates read) and get_plan_corrections (plan_correction). Plan Corrections is a newest-first list, so a capped page names the oldest recommendation it returns, and its "empty" answer also gets the note. The page's shared note strip learns three things: a spec can opt out (windowNote: false), which the newest-snapshot panels use (the two grants lists and Automatic Tuning); a stat tile can draw it as well as a table; and a spec can read the note from a nested block (floorKey), which the Query Store Clutter and Query Store Overhead panels use for the note their tool already returns. The shared fanout's empty path now draws the note as the single-panel path does.
Tests: page scenarios through the shipped util.js, panels.js and server-tabs.js in the Node harness (the opt-out, a stat, the nested note, the fanout's snapshot sibling, an empty Plan Corrections answer with and without the flag, Automatic Tuning quiet); the capped-rule tests extended to Plan Corrections; a live case for a memory read whose rows start inside the window; the admitted status words.
Part of #4966
* Daily Query Store summary for get_query_store_top's long windows: the tables (rung V161) and the hourly builder (#5094) (#5125)
Daily Query Store summary for get_query_store_top's long windows: the tables (rung V161) and the hourly builder (#5094)
Part of #5094. No reader uses the summary yet; the read and its "approximate" flag come next.
- Rung V161 adds two plain tables. collect.query_store_top_daily holds one row per server, UTC day and query identity (database, query id, plan id, query hash, outcome, replica role, module), with sums and non-NULL counts that recombine exactly, so a reader reproduces today's unweighted mean of interval averages. Its unique key is NULLS NOT DISTINCT. collect.query_store_top_daily_built records each built day's pass. The viewer's schema probe gains the V161 sentinel and arm.
- QueryStoreTopDaily is the hourly store-maintenance tick's fifth tenant. Pass 1 builds a day at its end + 2 h, pass 2 rebuilds it at its end + 25 h, and a day first seen past that point is built once at pass 2. Each build is one transaction under a 60 s statement timeout, bounded on first_execution_time to the day (−26 h, +1 h), for enabled servers and days after their coverage claim, oldest first. The tick does at most 60 builds and starts none after 10 minutes; GC follows the interval table's 9-day horizon and disabled servers.
- Approximate by design (Erik's call on #5094): a late write into a built day is missed, a row moving into the next day after pass 1 counts twice until pass 2, and a monitored server clock more than an hour ahead drops rows past the day's end + 1 h. Each case is pinned by a live test.
- On a production store, one build of the largest server's day read about 197k interval rows (30k blocks) into 26k groups in 2.3 s.
* Add a Manage Tags page to the web dashboard (#5085) (#5138)
The web dashboard gains a Manage Tags page, over the server-tag write routes (#5137). It reads the tag forest and each server's tags from the existing `/api/fleet`, so it adds no route, grant or schema change.
**What the page does.**
- **The tree:** up to four levels, with each tag's colour and its server count from the payload.
- **Tag actions:** create a root or child tag, rename, recolour, move under another parent, and delete.
- **Delete:** the page first asks the server without confirming. If deleting would leave custom alert rules matching no server, it names those rules and asks again, then sends `?confirm=true`.
- **Assignment:** a server checklist assigns the selected tag. The page records only the boxes the user changed, so Apply sends one assign for the added servers and one unassign for the removed ones, and never touches a server someone else changed meanwhile.
- **After each write:** the page lists the alert rules whose coverage changed.
- **The 60-second refresh** keeps the selection, the unsaved checkbox changes (marked "(unsaved)") and the open form, including the focused input.
- **In-flight writes:** while a write is running, a second click sends nothing, and Cancel and the form openers wait for it.
- **A parent deleted elsewhere** keeps the form and what was typed, and resets the parent to none.
- **Read-only sign-ins** see the page with every write disabled.
Every server string reaches the page through the house `el()` helper as text. A colour is used in a style only after it matches `#RRGGBB`.
**Tests.** The behaviour tests drive the page under Node with a fake fetch, and check each request's method, path, content type and body:
- the tree and the forms;
- the delete confirm flow;
- the assign and unassign diff, including servers changed elsewhere;
- the refresh keeping the form and the edits;
- double-click guards;
- 403, 404, 409, 415, 500 and network refusals;
- read-only.
Page tests pin the routes and methods.
Part of #5085
* get_read_latency fits its rows to the MCP response budget (#5135) (#5149)
`get_read_latency` returned up to `limit` rows (at most 1,000) and never fitted its response to the shared MCP response budget (`McpResponseBudget.DefaultBytes`, 32,768 bytes). With 1,000 routes, a `limit=1000` answer was 227,445 bytes.
The tool now builds its response the way the availability-group reader does:
- it takes rows in the total order (p95 descending, then count, then surface and route) while they fit;
- it then serializes the real final response, dropping the last row and re-serializing while it is still over the budget.
The answer can therefore never exceed the budget, even by the few bytes the counters add.
**New fields.** The response carries `truncated` (the limit or the budget cut the page), `reads_returned` and `reads_total`. Rows always drop from the end of the order. The guide tail says the page is capped at `limit` rows or the budget, whichever cuts first, and to raise `limit` or narrow with `surface` or `route`. The tool head is unchanged. The web read route returns the same capped answer.
**Tests.**
- A live test seeds 1,000 routes, then asserts the size bound, the three fields, and that the rows returned are the top of the order.
- A unit test sweeps the budget across the last row's edge, and asserts the UTF-8 size never exceeds it.
Fixes #5135
* MCP: Darling's four data-start twins, and Lite's config change, collection log, plan correction and memory pressure tools, say where their data starts (part of #4966) (#5147)
This lands two changes for #4966: the Darling MCP twins of Lite's data-start tools (first reviewed as #5131), and Lite's config-change, collection-log, plan-correction and memory-pressure tools.
Darling's get_active_queries, get_waiting_tasks, get_query_heatmap and get_query_store_regressions now say where their data starts, with the contract Lite's twins use (#5015): effective_start (UTC, with a Z), window_truncated and truncation_note right after hours_back on a data answer, and the same three keys under hints on an empty answer, so an empty or short answer over a window the store never covered no longer reads as a quiet one. not_collected and unavailable answers keep their shape. get_waiting_tasks, the heatmap and the regressions tool always carry the keys, as Darling's Queries tools do; get_active_queries carries them only when the window was cut, by the house rule that a payload omits null and false fields.
A shared helper, DarlingMcpWindowNotice, probes coverage through DataWindowFloor.GetAsync (the registry's created_date and the purge edge, a run counting as well as a row), with Lite's sentences copied and pinned. A window of 90 minutes or less with rows starts no probe; an empty answer always probes; the regressions tool probes from its baseline start. A probe that fails costs the notice, never the rows: it is logged and the answer goes without the keys. The web keeps its own note for the reads it lists (WebDataStartNote strips the tool's keys before deciding); the Active Queries, Query Heatmap and Query Store Regressions panels now show the tool's own sentence, as the Top Queries panels already do.
In both apps, get_active_queries now previews query text at 400 characters instead of 500, so a cut window (keys plus note) stays inside the 32,768-byte response budget: the default call over the budget test's 50 heavy rows is 30,223 bytes covered and 30,504 cut. get_active_queries' and get_waiting_tasks' descriptions now name the three keys in both apps.
Tests: helper tests (the 90-minute rule, the null floor, the exact sentences, a probe that throws, a caller who cancels); live tests for each tool (a notice when coverage starts inside the window, none on a quiet start, hints on an empty answer, a failed probe that keeps the rows); a covered and a cut budget case; the web guard; the census rosters and the shared-formatter pin. The full Darling suite fails only tests on the Mac-only list; Lite's description, budget and guide classes pass on Windows.
Lite's get_server_config_changes, get_database_config_changes, get_trace_flag_changes, get_collection_log (one server), get_plan_corrections and get_memory_pressure_events now say where their data starts, with the contract the other Lite window-floor tools use (#5004, #5015, #5127): effective_start (UTC, with a Z), window_truncated and truncation_note right after hours_back on a data answer, the same keys under hints on an empty answer, and not_collected and unavailable answers unchanged. The fleet form of get_collection_log names no server, so it carries no notice.
Each probe reads the source its tool returns rows from: the config snapshots on capture_time with their collector's runs, the collection log over the read's own window, v_plan_correction, and memory pressure on sample_time. None needs the event-time rule, because each probe already reads the column the rows are windowed on. The config change lists diff each in-window snapshot against the one before it, so a snapshot from before the window also counts as coverage, but only when the server was monitored during the window (at least one collection-log run); with no run in the window the answer says nothing was read (LocalDataService.GetConfigSnapshotCoverageFloorAsync). A plan-corrections answer that holds only automatic-tuning rows is probed as a data answer. The one-server collection log now treats a status filter as a filter, so "no ERROR runs" takes the filtered-empty answer rather than the quiet-window one.
Tests: McpWindowNoticeConfigAndLogToolTests (new): for each tool, a notice when collection starts inside the window, none on a quiet start, hints on an empty answer past coverage, the key order, a short window that starts no probe, a capped page where the tool pages; the config tools' pre-window snapshot with and without a run in the window; a status filter that matches nothing; a one-hour automatic-tuning-only answer. The census rosters and the shared-formatter pin cover the four files. On Windows, the three Lite window-notice classes pass (147).
Part of #4966
* Web grids: column groups with a toggle strip, first used on Memory Conditions and Node OOM (#5152)
Part of #4843
## What users see change
- The System Events **Memory Conditions** (32 columns) and **Memory Node OOM** (28 columns) tables now open narrow. A "Columns:" strip above the table has one toggle per column group, and a pressed toggle shows its columns.
- Memory Conditions: Event Time, Last Notification, Report and the OOM counters are always shown. Groups: Process memory (on at first), Pressure flags, Allocation detail.
- Node OOM: Event Time and the node ids and utilisation are always shown. Groups: Failure (on at first), Memory sizes, Pool detail, Flags.
- The choice survives the 60 s repaint. Another server's table starts from the defaults.
- Every other grid is unchanged.
## What changed
- `panels.js`: `vizTable` takes `groups` and `defaultGroups`. A column carries `group: "<name>"`; a column with no group is always shown. The toggle state sits in a module-scope map, keyed like the grid sort state (route, table identity, columns). A hidden column is still built and only set to `display: none`, so the sort, Copy and CSV column indexes stay aligned.
- `server-tabs.js`: `table()` takes an optional last argument `columnGroups = { groups, defaultGroups }`. The two wide column constants are grouped in the desktop grid's order, and #5118's "picker candidate" comments are gone.
- `css/app.css`: `.col-picker` rules beside the `.grid-tools` rules.
- No new module, so no harness needed an import change.
## Decisions
- **Copy and CSV export all columns, hidden ones included.** The desktop grid's export (`DataGridExport`) iterates every column of the grid, shown or not, so the web matches it. Copy cell and Copy row follow the same rule.
- **Sort:** hiding a sorted column keeps the sort. The order holds, the repaint re-applies it, and the other columns still sort.
## What's not changed
- No other grid gets groups. The wider Top Queries and Query Store grids are for the following lanes, which can reuse this option.
## Verified at the head
- New `ColumnGroupsBehaviourTests` (node, 6 tests): no groups gives an unchanged table, defaults, toggling, rebuild and server switch, sort with a hidden column, and Copy carrying all columns. RED on dev: with dev's `panels.js` four scenarios crash with no picker and the fifth shows no strip. 6/6 green now.
- The no-groups case asserts the DOM is dev's: children are exactly `grid-tools`, `table-wrap`; no th or td carries a `display` style; `tools:false` returns the bare `table-wrap`.
- The sort case uses a fixture where A's order (y, x, z) differs from B's (y, z, x). With the comparator forced to return 0, the check fails (`byA` comes back x, y, z).
- New `ServerTabColumnGroupsPinTests` (2): each grid's groups match its group list and defaults, and each constant is the last argument of its own `table()` call (Memory Conditions, Memory Node OOM). 2/2.
- `ServerPageTabsTests` 19/19, `WebModuleLoadTests` 7/7, `GridToolsBehaviourTests` 9/9, `PgIndexBloatCoverageTests` 16/16 (this file is not changed by the PR; its `span, noteKey }` pin still matches because the `table()` literal keeps that text).
- The wider guard family and every `.mjs`-harness class: 239 classes run on this Mac. The one failure, `LivePostgresCollectionHygieneTests`, is in `dev-local-failing-tests.txt`.
- `ServerPageTabsTests` was updated: it now lists `columnGroups = null` in the `table()` signature.
- `scrubcheck` on the diff exits 0.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Column toggles on the wide System Events grids** ([#5152]) - Memory Conditions and Memory Node OOM open with a short default column set and a "Columns:" strip that reveals the rest, and the choice survives the page's refresh.
REF: [#5152]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5152
* The web Alert History page gains Dismiss Selected and Dismiss All (#5151)
Part of #4843
## What users see
On the web Alert History page, a signed-in seat that can edit now gets:
- a checkbox on every live row;
- a **Dismiss Selected** button (it shows the checked count);
- a **Dismiss All** button that dismisses every live row the current filter box shows, after the Darling Viewer's prompt ("Dismiss N alert(s)? … hidden from this view but remain in the store"). When the page is truncated (more alerts than the row limit shows), the prompt adds that only the listed alerts are dismissed and the rest stay live.
After a dismiss the list refreshes. With Show dismissed off the rows disappear. With it on they stay and say "Dismissed". A short status line reports the counts, for example "Dismissed 12 of 12", or "Dismissed 1 of 4; 3 were already gone". A refusal (403, 401, 400, 5xx) shows the server's text in the status line and the page never claims success. A read-only seat sees none of this.
## What changed
- `alerts.js`: the select column, the dismiss bar and the write, which goes through the shared `apiSend` helper (JSON content type, session-expiry handling) to `POST /api/alert-history/dismiss` (#5106). The body is `{alerts:[{alert_time, server_id, metric_name}]}`.
- The checked rows live at module scope, keyed by the row key. They survive the 60 s rebuild and a visit elsewhere and back. After a successful dismiss only the keys that were sent are removed, so a box ticked while the request was in flight stays checked. A failed or empty poll read keeps the set; only a successful read prunes rows that left the list. The set is cleared when the range, row limit, server, Show dismissed or filter box changes.
- Dismiss All sends the listed rows, with no new route. This differs from the Viewer, whose Dismiss All covers every live alert in the window, even past the row limit; the web keeps its listed-rows design and says so in the prompt when the page is truncated. The route caps a request at 1000 keys, so a longer list goes in chunks of 1000 and the counts are added up. A failure part-way says how far it got and refreshes the list.
- Dismissed rows, and rows with no `server_id`, get no checkbox and are not sent.
- `panels.js`: a column flag `copy: false` leaves a column out of Copy row and Copy all (the CSV already honours `csv: false`). The Select column sets it; the Mute column is unchanged. Copy cell and the picked-cell lookup still see every column.
- `css/app.css`: three rules beside `.filter-box`.
- The #5109 range controls and the #5110 `markDismissed` / `tools: false` row build are untouched.
## What's not changed
No server code, no other page, `util.js` or `server-tabs.js`; in `panels.js` only the `copy` flag. There is no "since" route.
## Tests
- New `AlertHistoryDismissBehaviourTests` (13, with `alert-history-dismiss-harness.mjs`, run under Node on the shipped page): no controls for a read-only seat; Dismiss Selected posts the right keys and refreshes; Show dismissed keeps the row with "Dismissed"; Dismiss All posts what the filter shows; chunking at 1000; the checked set surviving a poll and a re-open and clearing on a filter change; a 403 and a 401 shown and the selection kept; the counts line; the prompt wording and the truncated-page line; Copy row and Copy all without the Select column and with Mute kept; a failed poll read keeping the checked set; a box ticked mid-dismiss surviving unsent.
- `AlertHistoryRangePageTests` dropped its "page has no Dismiss Selected" assertion. `MuteRulesPageTests` pins the new column expression. The range harness's metric column index moved from 2 to 3 because of the select column.
## Verified at the head
- RED on dev: with `alerts.js` reset to dev, 8 of the original 9 new tests fail; the four added for the prompt, Copy and selection handling also fail there. The read-only test passes there because it has nothing to show.
- GREEN: `AlertHistoryDismissBehaviourTests` 13/13, `AlertHistoryRangePageTests` 12/12, `MuteRulesPageTests` 6/6, `WebModuleLoadTests` 7/7. `load-web-modules.mjs` loads all 45 modules.
- Every `.mjs`-harness class and 104 classes in total (those plus the whole Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention family): one failure, `LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot`, which is on `dev-local-failing-tests.txt`.
- The guard-family run above covers 238 classes, including `GridToolsBehaviourTests` 9/9 (the shared renderer changed) and `WebModuleLoadTests` 7/7. The earlier full-suite run was before the review fixes; the full suite is left to CI. 0 build warnings.
- `scrubcheck` exits 0 on the diff and on this file.
- Not run: nothing required. Lite.Tests is untouched.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Dismiss Selected and Dismiss All on the web Alert History page** ([#5151]) - a seat that can edit gets row checkboxes and the desktop's two dismiss buttons, with a status line of how many alerts were dismissed.
REF: [#5151]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5151
* Web: cross-server Job History page, plus a Job History grid on the Activity tab (#5150)
Part of #4843
## What users see change
- A new **Job History** entry at the end of the web nav opens a page showing SQL Agent job runs (steps and job outcomes) across the fleet, newest first, the web twin of the desktop Job History view.
- Columns follow the desktop grid: Run Time, Server, Job, Category, Step, Status, Duration, Retries, Last Success, Message. Sort, Copy and CSV come from the shared grid.
- Filters: Server (all or one), Window (1/4/12/24 hours, 7 days; default 24 hours), Job (exact name, applied on Enter or when the box loses focus), Status (all, Failed, Succeeded, Retry, Canceled), Category (the categories seen so far), and Rows (100/250/500/1000, default 100). All filters are applied by the read before its row limit. Choices, typed text and focus survive the 60 s refresh.
- The page says when the window reaches past the history the store keeps (also when the answer is empty), and when the row limit cut the list. A "N runs shown" line sits above the grid.
- Rows are coloured as the desktop grid does: failed red, long-running and retry amber, canceled grey. The desktop also dims disabled jobs; the read does not return an enabled flag on this path, so the web does not.
- Differences from the desktop tab: the windows are 1/4/12/24 hours and 7 days (the desktop also has 30 d, 90 d and 1 y; the read's maximum is 168 hours), and there is no 2,000-row cap, the Rows choice goes to 1000 at most.
- The Server choices list SQL Server targets only (PostgreSQL targets have no Agent history).
- The server page's Activity tab (SQL Server) gains a per-server Job History grid with the same columns less Server (newest 100 runs in the page range). It was not on the web before.
## What changed
- `pages/job-history.js` (new), reading `get_job_history` through `readToolWithinKeptHistory` with the render's `AbortSignal`; server choices from `list_servers`.
- `index.html` and `app.js`: one nav entry, import, route, active-nav key, each added at the end of its list.
- `server-tabs.js`: the Activity-tab grid and its column list only.
- `job-history-harness.mjs` and `JobHistoryPageTests.cs` (15 tests).
- `app.css`: three row-colour rules beside the data-table cell colours. `util.js`: `run_time` added to the instant fields, so a custom range trims the per-server grid. No C# product change.
- The job box defers its blur by a turn, so a rebuild no longer drops focus or fires a second read, and the caret is kept. The empty-answer notice reads `hints`.
## What's not changed
`panels.js`, the rest of `util.js`, `alerts.js`, the `app.js` scheduler and status bar, other `server-tabs.js` regions. The PostgreSQL Activity tab has no Job History (Agent history is SQL Server only). The desktop's Agent status indicator is not on this page (it needs a separate `get_agent_status` read).
## Verified at the head
- `JobHistoryPageTests`: 15/15 pass; all FAIL on dev's wwwroot (page and wiring removed).
- `WebModuleLoadTests` passes; module count comes from disk.
- Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention family (79 classes) plus every `.mjs`-harness class (26): 1255 run, 1 failed, `LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot`, which is in `dev-local-failing-tests.txt`.
- Full Darling.Tests (run at the previous head; this push re-ran the page, harness, load and guard classes only): 22703 run, 478 failed (377 distinct methods). All are on `dev-local-failing-tests.txt` except two `DarlingManagedPostgresTests.EnsureConfAppended_*` methods; they do not touch web files and I did not run them on a clean dev, so CI decides.
- `scrubcheck` on the diff and this file: exit 0.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Job History page in the web dashboard** ([#5150]) - A cross-server Job History page shows SQL Agent job runs with server, window, job, status, category and row-limit filters, and the Activity tab gains a per-server Job History grid.
REF: [#5150]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5150
* The web dashboard shows a query's stored plan XML from Top Queries, High Impact and Expensive Queries (#5156)
Part of #4843
## What users see
Rows that carry a `query_hash` get a **Plan** button in Top Queries (server page), FinOps High Impact and FinOps Optimization > Expensive Queries. It opens a panel under the row with the query's stored plan XML, indented, with **Copy** and **Download .sqlplan** (file named `<query_hash>.sqlplan`, type `application/xml`). A plan larger than the read's 500 KB cut says so and turns Download off, because a cut file will not open. A query with no stored plan shows the read's own message. The open panel stays open through the 60 s rebuild.
## What changed
- New `pages/plan-viewer.js`: `planColumn(server)` (added to each grid's column list), `storedPlanCell`, `openStoredPlan`, `prettyPrintXml`, `downloadPlan`. It reuses `copyText` from `grid-tools.js`. The XML is drawn only through `textContent` of a `<pre>`.
- `get_plan_xml` returns bare XML for the MCP clients (unchanged). The web read route wraps a stored plan as 200 JSON `{query_hash, database_name, plan_xml, truncated}`; `truncated` is decided on the server from the tool's own cut marker, which is removed from `plan_xml`. A "no plan" status or error envelope passes through unchanged. The viewer reads `plan_xml` and `truncated`; it no longer sniffs text. Params: `server`, `query_hash`, and `database_name` when the row has one.
- Expensive Queries rows now carry `query_hash` (`MAX(query_hash)` in the existing statement query, a new optional `QueryHash` on the `ExpensiveQuery` record, and a `query_hash` field in the `get_finops` optimization view's expensive-query rows), so the Plan column shows there. No tool description changed.
- Open panels are keyed by server + database + hash at module scope; a rebuilt cell redraws from that state, and a read still in flight fills the new cell too.
- Top Queries (both the tab and the standalone panel), High Impact and Expensive Queries use `[...COLUMNS, planColumn(server)]`; the column hides when no row has a hash. `css/app.css` gets three rules beside the grid-tools rules.
- Every `.mjs` harness that copies `server-tabs.js` or the finops tabs now also copies `plan-viewer.js`.
## Not changed
Get Actual Plan and a plan tree view stay out. The PostgreSQL Top Query Shapes grid has no Plan column: `get_plan_xml` is SQL Server only (pinned). No `panels.js`, `util.js`, `app.js` or `alerts.js` edits.
## Verified at the head
- Web wrapper: 3 new `DarlingWebEndpointsTests` cases (whole plan gives JSON that classifies as passthrough; a cut plan sets `truncated` using the real `McpHelpers.Truncate` marker; envelopes pass through byte-identical). `DarlingWebEndpointsTests` 76/76. The plan harness now fakes the 200 shape. `FinOpsTabOptimizationPageTests` pins the new `query_hash` row field.
- `WebExceptionTextCensusTests` 40 (1 skipped), `DarlingCustomViewsTests` 71/71 (`get_plan_xml` params unchanged; a custom view calling it now gets a `plan_xml` JSON object instead of a 400 error strip), `McpToolsListBudgetTests` 5/5, `FinOpsOptimizationViewTests` 8/8.
- RED on dev: `WebPlanViewerPageTests` 5 of 6 fail, `PlanViewerBehaviourTests` 7 of 7 fail (module absent).
- GREEN: `WebPlanViewerPageTests` 6/6, `PlanViewerBehaviourTests` 7/7 (open, text-only, download name/type/body, copy, truncation disables Download, no-plan text, state survives rebuild).
- Updated import allowlists in `FinOpsTabHighImpactPageTests` (8/8) and `FinOpsTabOptimizationPageTests` (16/16); `RepoFileAdoptionTests` 2/2.
- All 100 `.mjs`-harness and Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention classes run; only `LivePostgresCollectionHygieneTests` fails, which is on the Mac-local dev failing list.
- Build 0 errors, no new CA/CS/IDE warnings.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Stored query plans on the web dashboard** ([#5156]) - Top Queries, High Impact and Expensive Queries rows with a query hash get a Plan button that shows the stored plan XML with Copy and Download .sqlplan.
REF: [#5156]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5156
* PostgreSQL plans are grouped by their shape, so estimate drift no longer reads as a plan change (fixes #5114) (#5155)
PostgreSQL plans are now grouped by their shape. The plan hash (PgPlanLogParser.FromBlock's PlanHash) covered the whole redacted plan JSON, cost and row estimates included, so a statistics refresh, a different literal or a different work_mem gave one plan shape a new hash, and the plan-flip finding read estimate drift as a plan change. The hash now covers the shape only: it is taken over a clone of the redacted plan with the estimates, the runtime counters (Actual *, Rows Removed by *, Index Searches, Heap Fetches, sort, hash and memoize counters, Workers and Workers Launched), the buffer, I/O and WAL counters, Workers Planned, and the root's Settings, JIT, Planning, Planning Time, Execution Time and Triggers removed, recursively through every child plan. It is a denylist, so an unknown key still splits the hash. The stored plan_json is unchanged and keeps every estimate. A plan without any of those fields hashes as before.
Captured-plan lists (get_pg_plans and the viewer) fold a shape's estimate variants into one row, so captures and durations sum over the shape. Each row now shows the JSON of that shape's latest collection cycle: the reader ranks and cuts the groups first and fetches the JSON for the page only, instead of the old min(plan_json), which was arbitrary once a group held JSONs with different estimates, and aggregated kilobyte-sized text over every group.
On the first analysis pass after the upgrade, a statement captured on both sides of it may report one plan change, and captured-plan lists and the store log's slow_plan groups show pre-upgrade groups beside new ones until the older rows age out (plan retention is 14 days by default).
Tests: PgPlanShapeHashTests (new, 29 cases): estimate, settings, timing and counter variants give one hash, including on a grandchild and a SubPlan node; a different node type, join order, join method, index or parallel awareness gives another; Workers Planned is not shape; an unknown key still splits; the stored JSON keeps its estimates; the hash stays 32 hex characters. A store-log case (two plans differing only in costs are one row) and a live reader case (one group, two captures, the later capture's JSON).
Fixes #5114
* Record the target session id on SQL Server collector runs (#5132) (#5154)
`collection_log.target_session_id` was null on every SQL Server collector run. Monitored-server connections use Multiple Active Result Sets, and under MARS `SqlConnection.ServerProcessId` is 0 at every point: after open, with a reader open, after it closes, and on pool reuse. Measured on SQL Server 2025 with Microsoft.Data.SqlClient 7.1.1. With MARS off it is the real session id.
**The fix.** Right after a collector's target connection opens, before any reader, the runner resolves the session id:
- When `ServerProcessId` is 0, it runs `SELECT @@SPID` once, with a 5-second command timeout, and caches the answer under the connection's `ClientConnectionId`.
- That id stays the same across pool reuse, so a reused connection costs no query, and the cost is one lookup per new physical connection rather than one per run.
- The cache is bounded at 4,096 entries.
- The lookup is timed into the run's SQL time.
- A failure records null and never fails the run. A real cancellation still propagates.
- If SqlClient transparently replaced the physical connection by the time the first reader opens (a new `ClientConnectionId`), the run records null instead of a stale id.
- PostgreSQL targets still read the backend pid directly.
MARS, the pools and the application name are unchanged.
**Why not the alternatives.** Turning MARS off would break the places that run a second command while a reader is open. A per-collector application name would multiply the connection pools on every monitored server.
**Tests.**
- Unit facts for the cache: one lookup per connection id, the bound, failures not cached, a real cancellation, the key read after the lookup, and eviction of a replaced connection.
- A source pin: the lookup comes before every reader.
- Live facts against a real SQL Server: the true session id; no second lookup on a pooled reopen; a full collector run writing a non-null `target_session_id` into its own scratch store.
CI runs no SQL Server, so it skips the live facts. They were run locally.
Fixes #5132
* Web Blocking tab: desktop columns on the Blocking grid, and Save XML on Blocked Process Reports and Deadlock Graphs (#5164)
Part of #4843
## What users see
On a server page's Blocking tab:
- **Blocking grid** now shows the desktop Blocked Process Reports grid's columns, in its order and under its headers (Event Time, Blocked SQL, Blocking SQL, Source, Database, Blocked SPID, Blocking SPID, Wait Time, Wait Resource, Lock Mode, then status, isolation, transaction, priority, log used, login, host and app for each side). It has 25 columns, so the status/isolation, transaction and sessions columns sit behind the column-group toggles from #5152 (status/isolation and transaction on at first).
- **Blocked Process Reports** panel: desktop headers (Event Time, Blocked SPID, Blocking SPID, Wait Time) and a **Save XML** button per row, downloading `blocked_process_<yyyyMMdd_HHmmss>.xml`.
- **Deadlock Graphs** panel: a **Save XML** button per row, downloading `deadlock_<yyyyMMdd_HHmmss>.xdl` (the item #5139 deferred until a download helper existed).
- A deadlock graph the read cut short (`deadlock_graph_xml_truncated`) gets a disabled button with a hover reason. The tab reads with `full_graph` on, so this is the rare case. A row with no XML also gets a disabled button.
## What changed
- `grid-tools.js`: new `downloadText(fileName, parts, type)`; `downloadCsv` now calls it (same Blob, same type).
- `server-tabs.js` (Blocking/Deadlock regions only): `BLOCKING_COLUMNS` (11 -> 25) with `BLOCKING_GROUPS`; `BPR_COLUMNS` reordered with the desktop headers and an XML column; `deadlockXmlColumns` gets the XML column; `saveXmlButton` and `fileStamp` helpers; one import.
- No fetch on click: both reads already return the full XML on the row, so nothing is in flight. There is therefore no in-flight state.
- No harness `.mjs` import list changes: `grid-tools.js` is already in every harness's conditional copy list.
- Tests: `WebBlockingColumnsPageTests` (column keys against `get_blocking` / `get_blocked_process_xml` projections, desktop order and headers, group names, file names); `WebBlockingSaveXmlBehaviourTests` + `web-blocking-save-xml-harness.mjs` (Node: Blob name, type and text for both downloads, truncated disables Save, no-XML disables Save, no extra read).
## Not changed / read-widening follow-ups
- The desktop's Blocked Process Reports grid has no column the read lacks. Not on the web grid: the blocking-side transaction/priority fields, which the desktop does not show either.
- `get_blocked_process_xml` returns only 6 fields, so the Blocked Process Reports panel stays at the desktop's four data columns; the desktop's extra columns live on the Blocking grid (from `get_blocking`).
- **`.xdl`, not `.xml`:** the Viewer and Lite save deadlock graphs as `.xml`; the web uses `.xdl`, which SSMS opens as a graph (the deprecated Dashboard did the same). A follow-up can move the desktop to `.xdl` too.
- Reads not widened.
## Verified at the head
- New page tests: 6/6 pass; node behaviour tests: 4/4 pass. On dev's JS (`git checkout` of the two files, rebuilt tests kept): page 4/6 fail, behaviour 4/4 fail (RED).
- Full Darling.Tests on this Mac: Total 22912, Failed 478 (336 distinct methods); 334 are in `dev-local-failing-tests.txt`. The other two, `DarlingManagedPostgresTests.EnsureConfAppended_*` (2 methods), fail with `kernel32.dll` not loadable on macOS (a platform failure, nothing to do with this change).
- That run included `WebModuleLoadTests`, `DeadlockProcessRowsTests`, every `.mjs`-harness class and the Census/Guard/Hygiene family, which showed no failures beyond the above.
- Build: 0 warnings, 0 errors.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Web Blocking tab shows the desktop's blocking columns and saves report and deadlock XML** ([#5164]) - the Blocking grid has the desktop Blocked Process Reports columns (the wide ones behind column-group toggles), and the Blocked Process Reports and Deadlock Graphs rows have a Save XML button that downloads the XML (`.xml`) or the deadlock graph (`.xdl`).
REF: [#5164]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5164
* Web Perfmon: multi-select counter picker over a multi-series trend chart (#5162)
Part of #4843
## What users see change
The Perfmon chart on a server's page used to chart one counter chosen from a drop-down. It now matches the desktop viewer's picker and the Wait Stats picker from #5119:
- a checkbox list of the counters in the snapshot table above it, with a search box, Select All, Clear All and a Default counters button;
- the default set is the desktop's General Throughput pack (Batch Requests/sec, SQL Compilations/sec, SQL Re-Compilations/sec, Query optimizations/sec, Network IO waits), limited to what this server collects; if none of the pack is collected the first counter is checked;
- up to 10 counters at once (the 11th box is disabled with a "Limit reached" hint; the desktop's cap is 12);
- one line per checked counter on one zoomable chart. Rate counters plot their per-second figure on a per-second axis; a chart that mixes in a gauge or average counter plots plain values with no unit;
- a counter whose read fails gets a note above the chart and the other lines still draw; all reads failing shows an error plus the notes; all reads empty shows the empty state, not an error;
- checks and search text survive the 60 s refresh, per server; a checked counter that leaves the snapshot stays checked and listed; the search box keeps focus and caret.
## What changed
- `server-tabs.js`, Perfmon region only: `perfmonPanel` and `drawPerfmonTrends` (replaces `drawPerfmonTrend`; `perfmonTrendLines` is gone). One `get_perfmon_trend` read per checked counter, in parallel, with an AbortController per server for superseded draws, and the chart draws at most 10 counters whatever it is handed.
- `multi-picker.js`: three optional options (`noun`, `defaultsLabel`, `defaults`) so the wording and default set are not wait-specific. Defaults are the old values, so Wait Stats is unchanged.
- `BaselineDiscontinuityRenderCensusTests` (Lite) now looks for `export async function drawPerfmonTrends(`. Its logic still holds: that function keeps one `discontinuityNotes(trend.data)` use, and the file total stays 4. Read by hand; Lite.Tests was not run on this Mac.
- New `WebPerfmonMultiSelectBehaviourTests` (14) with `web-perfmon-multi-harness.mjs`; `WebPerfmonPerSecondBehaviourTests` expectations changed from "Per second"/"Value"/"Delta" series to one line per counter.
## What is not changed
- The snapshot table, its per-second columns, and `get_perfmon_trend`. The read takes a counter name and no instance, so the list is by counter name (the desktop also lists by counter name). The desktop's other packs are not offered.
- `panels.js`, `util.js`, `charts.js`, `app.js`, CSS (the existing picker rules are enough).
## Also
- The trend harness copies `pages/plan-viewer.js` when it exists, so it keeps loading once the plan viewer import lands in `server-tabs.js`.
- When none of the default pack is collected, the first counter is checked (the desktop checks none); this continues the page's earlier behaviour.
## Verified at the head
- `WebPerfmonMultiSelectBehaviourTests`: 15 pass (one new: a counter with no trend lists the collected counters as text, "Collected here: …", instead of pointing at a JSON path). `WebPerfmonPerSecondBehaviourTests`: 10 pass. `WebWaitStatsMultiSelectBehaviourTests`: 14 pass, unchanged.
- RED on dev: the new harness cannot pass against dev's `server-tabs.js` (no picker, no `drawPerfmonTrends`; the `defaults` scenario fails at the missing Select All / mp-count node).
- Every `.mjs` / `load-web-modules` class, `WebModuleLoadTests` and the Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention family (104 classes, 1,300 tests): 1 failure, `LivePostgresCollectionHygieneTests` (Mac-local; it cannot load WPF types here).
- `scrubcheck` on the diff: exit 0.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Web Perfmon chart charts several counters at once** ([#5162]) - the chart now has the desktop's checkbox list with search, Select All, Clear All and the General Throughput default set, up to 10 counters on one zoomable chart. Your choices are kept across the page's refresh.
REF: [#5162]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5162
* WEB: the PostgreSQL tabs' window aggregates and trend grids say where their data starts (part of #4966) (#5165)
The web server page's PostgreSQL tabs now say where their data starts on eleven more reads: top queries, blocking, database stats, session states, I/O stats and replication stats, the database, query-duration, wait and I/O trend grids, and captured plans. WebDataStartNote lists each read with the table its rows come from; blocking, session states and replication stats are sparse (most samples store nothing) and have no retention edge, so they are probed on their own collector's runs instead (CollectorRunsByRead), and a long-monitored server whose first blocking chain or replica came recently reads as covered.
Each read's "captured, found nothing" status word is admitted where it has one (no_blocking_sampled, no_io_activity, empty for the database, session-state and trend reads), never not_sampled, unavailable or precondition; tools that answer rows with a status word and no message (session_states, blocking_sampled, cycles_only, the trend words and others) are accepted as row answers. No stat tile beside these reads shows only newest values, so no panel opts out, and server-tabs.js is unchanged.
Tests: the live PostgreSQL theories cover the 17 noted tables; a sparse read on an old server with one late row gets no note, and on a two-day-old server gets one; the status words are pinned from the tool sources and by behaviour; 11 page scenarios through the Node harness; the source pin is scoped to the listed reads.
Part of #4966
* Web line charts get a menu: copy/save image, export CSV, show data source, reset zoom (#5166)
Part of #4843
## What users see
Every web line chart has a small "⋯" button at its top right, and right-click on the chart opens the same menu. Items, in the Viewer's order and wording:
- **Copy Image**: draws the chart to a PNG and puts it on the clipboard (secure page only; otherwise a plain message says why).
- **Save Image As...**: downloads the PNG.
- **Reset zoom**: only while the chart is zoomed (#5092).
- **Export Data to CSV...**: one row per time and series (`DateTime (UTC),Series,Value`). It exports every loaded point, also while the chart is zoomed, the same as the Viewer. Times are UTC in three columns; the Viewer writes the display time zone plus a UTC-offset column.
- **Show Data Source**: the read name and its parameters, shown under the chart. Hidden when the chart has no read to name.
The menu is keyboard reachable: Tab to the button, Enter to open, Arrow keys to move, Escape or Tab to close. A right-click menu opens at the pointer and stays inside the chart. The open menu and the Show Data Source panel stay open across the 60 s refresh (kept at module scope per chart and page scope, for 90 s).
## What changed
- `charts.js`: `renderLineChart` attaches the menu, so every chart gets it. New optional spec fields `title`, `source` ({ read, params }), `zoomed`, `onResetZoom`. `zoomableLineChart` supplies the zoom pair. No new exports are imported by any other module.
- `panels.js`: the panel chart passes its title and its read name and params (two lines).
- `css/app.css`: menu rules beside the chart tooltip rules.
- CSV reuses `grid-tools.js` (`toCsv`, `csvFileName`, `downloadCsv`), loaded on first use. PNG: the SVG is cloned with computed styles written inline, drawn on a canvas, no external fetches.
- Copy Image hands the clipboard a promise for the PNG (Safari needs the write started inside the click); a failed CSV module load shows "Export failed".
- Tests: `WebChartMenuBehaviourTests` + `web-chart-menu-harness.mjs`.
## Not changed
- **Open in New Window** is out of scope.
- Server-tab and FinOps charts that call `zoomableLineChart` directly pass no `source`, so they show no Show Data Source until a caller passes it.
- Bar, pie and scatter charts have no menu.
## Verified at the head
- New harness on dev's `charts.js`: RED (the menu does not exist; the harness throws). On this head: 8/8 pass (adds right-click placement `right:auto` + clamped left/top, zoomed CSV = all 11 points, Tab closes, menu and source survive a rebuild).
- Every `.mjs`-harness class (incl. `WebModuleLoadTests` 7/7, `WebChartZoomBehaviourTests` 10/10, `GridToolsBehaviourTests` 9/9): 0 failures. `ChartIntegerTicksPinTests` needed the spec destructure kept as is; passes 3/3.
- This run: the targeted classes, every `.mjs` class and the guard family, 113 classes, 1364 tests, 1 failure.
- Guard family (all classes ending Census/Ratchet/Adoption/Discipline/Guard/Scrub/Hygiene/Pin/Convention Tests, plus `DocCommentHygieneTests`): only `LivePostgresCollectionHygieneTests` fails, and it is on `dev-local-failing-tests.txt`.
- Full Darling.Tests on this Mac: 22847 total, 478 failed, 1920 skipped. Every failing class is on the Mac-local list (parameterized rows and `ViewerChartContextMenuTests`, WPF); none touch charts.js.
- Not run in a real browser: the canvas, clipboard and menu placement are untested there (the node harness has no canvas or layout); worth one manual click-through.
- scrubcheck on this file and the diff: exit 0.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Web line charts have a menu** ([#5166]) - Each line chart has a "⋯" button (and right-click) with Copy Image, Save Image As, Export Data to CSV, Show Data Source (where the chart knows its read) and Reset zoom (while zoomed).
REF: [#5166]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5166
* get_server_trend: latch, spinlock, session stats and collector duration trends (#5170)
Part of #4843
## What users see change
`get_server_trend` (and its `/api/read/get_server_trend` route) takes four more `metric` values:
| metric | a point holds |
|---|---|
| `latch` | `wait_time_ms_per_second`, one series per latch class |
| `spinlock` | `collisions_per_second`, one series per spinlock |
| `session_stats` | the server-wide session counts (`total_sessions`, `running_sessions`, `sleeping_sessions`, `background_sessions`, `dormant_sessions`, `idle_sessions_over_30min`, `sessions_waiting_for_memory`, `databases_with_connections`), averaged per bucket, plus the top application and host of the newest collection in the bucket |
| `collector_duration` | `max_duration_ms`, `avg_duration_ms`, `run_count` of successful runs, one series per collector |
- **New `names` parameter** for `latch`, `spinlock` and `collector_duration`: comma-separated, exact case, up to 10, default the top 5 (most wait time, most collisions, longest run). Names with no samples come back in `missing_names`. If none of the named ones has a sample, the answer is `empty` with a distinct note, `missing_names` and `top_names`. `names` on any other metric is refused, and `clerk_types` on these three is refused.
- Latch and spinlock rates use the viewer's rule: summed delta over the summed stored interval, and a restart's unknowable (interval 0) sample is left out rather than drawn as 0.
- Same envelope as before: house window and bucket parameters, omitted nulls, `discontinuities[]`, and `empty` / `unavailable` / `not_collected`. `session_stats` is sized as four single-field points so its default 168 h answer stays under 32 KB.
## What changed
- `ServerTrendSql.cs`: `SessionSummary` (moved verbatim from the viewer), `LatchWaits(n)`, `SpinlockCollisions(n)`, `CollectorDurations(n)`, the three top-N reads and four "ever collected" probes. The viewer's `SessionStatsSql` now points at `ServerTrendSql.SessionSummary`; I diffed the old and new text and they are identical (71 lines).
- Latch, spinlock and collector duration: the viewer's reads have a different shape (per-row rates for a fixed top 5; per-collector buckets with no name filter), so the bucketed, name-filtered reads are written new in the house style. The viewer's SQL is untouched. One new read per metric, no new Storage reader class.
- `DarlingMcpServerTrendTools.cs`: the four metrics, `names`, a shared series path for clerks, latch, spinlock and collectors, notes and `Fields` per metric. `DarlingWebEndpoints.cs`: `names` in the catalog row and dispatch. `GetServerTrend`'s internal overload takes `names` before the budget so the cancellation token stays last (CA1068).
- Budget txt, ceiling, and tests.
## What's not changed
No `wwwroot`, no `DarlingManagedRoles.cs`, no migration, no change to the four earlier metrics' output. Left for a later change: `tempdb_file_io`, `tempdb_size`, `file_io_throughput`.
## Budget
- `tools/list`: 186,010 -> 186,158 bytes (+148); `TotalCeilingBytes` set to 186,158.
- Per-file txt: tool head 279 -> 331 (text grew after the guide marker mostly), `metric` param 61 -> 44 (the list moved into the tool description), new `names` param 70.
- Largest default call at 168 h, on a week of one-minute collections (8 latch classes, 8 spinlocks, 12 collectors): latch 11,127 B, spinlock 10,831 B, session_stats 17,944 B, collector_duration 15,627 B. All under the 32 KB target. The four earlier metrics are unchanged (largest 17,555 B, cpu_scheduler). Widest explicit width: latch 106,172 B at 34 min, spinlock 103,197 B at 34 min, session_stats 149,898 B at 27 min, collector_duration 149,991 B at 34 min, all under the 320 KB cap.
## Verified at the head
- Build: Darling.Tests Release, 0 errors, no CA/CS/IDE warnings after the CA1068 fix.
- `DarlingMcpServerTrendToolsTests`: 13/13 (parameters including `names`, eight-metric list in the description, refusals, every field a note names is emitted, `missing_names` in the description, name/width binding in the SQL, viewer and tool share the session SQL text).
- `DarlingMcpServerTrendToolsLiveTests`: 2/2 on a throwaway TimescaleDB 2.30.1 / PG18 container (host port 55615, removed). The new arm covers planted values for all four metrics (100 ms/s latch, 20/s spinlock, 100 then 120 sessions with the newest collection's application, 9000 then 9100 ms slowest collector), the restart-marker sample dropped as a point, a failed run not counted, `missing_names`, the none-found empty note, `unavailable` before any collection, `empty` for a quiet window, and the 168 h sizes above. Run 4 times; one run failed once on `bucket_minutes` auto sizing (fixed by pinning 1 minute in the planted arms) and is stable since.
- `WebReadCancellationPinTests` 139/139 (its `metric=total_waits` query string still works; the new parameter is optional).
- `McpToolsListBudgetTests` 5/5, `McpToolGuideTests` 10/10, `McpToolGuideHeadsDataTests` 3/3, `BaselineDiscontinuityRenderTests` 3/3, `EngineCapabilityReadWiringTests` 6/6 (the capability question names `latch_stats`, `spinlock_stats` and `session_stats`; `collector_duration` asks no collector because the log is not a gated collector).
- Guard family: all 79 Darling.Tests classes ending in Census, Ratchet, Adoption, Discipline, Guard, Scrub, Hygiene, Pin or Convention + Tests, plus DocCommentHygiene, RepoFileAdoption, TsqlConventionGuard, FleetIdentifierScrub, QueryStoreSliceTieBreakSource, the three command-timeout classes and the viewer session/duration/latch classes: all pass except `LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot` (loads WPF types; on the Mac-local list) and `ViewerCollectorDurationChartTests.TheChart_IsFedItsOwnRead_NotTheGridsPage` (PresentationFramework; on the list).
- Full Darling.Tests once: 22,847 run, 552 failed, 247 skipped. All failing names (430 distinct after dropping theory-row arguments) are on `dev-local-failing-tests.txt`; none is new.
- RED on dev: `Metrics` has four entries, `names` and `SeriesOf`, `ServerTrendSql.SessionSummary`/`LatchWaits`/`SpinlockCollisions`/`CollectorDurations` do not exist, so the new tests fail to compile there (RED by compile failure). Before the sizing fix, session_stats was 68,530 B at 168 h, over the 32 KB target.
- Not run: Lite.Tests (never run on this Mac; the change touches no Lite file).
- Finds: 0 substantial, 1 hygiene fixed in-lane (session_stats default width sizing), 0 polish.
- Follow-up at head 96df107d5 (dev merged in): a latch or spinlock row with a known interval but a NULL delta no longer adds its seconds to the rate (the Viewer drops that row too); `KindMetric` is gone and `ReadTopNamesAsync` always takes the metric. New live test `TheRateRules_SessionNewestCollection_AndCollectorNames_HoldAgainstTheStore` covers the NULL-delta row (RED before the fix: 50 ms/s instead of 100), a latch row with no stored interval (previous-collection gap), two session collections in one bucket (the newest one's attribution) and `names` for collector_duration. `DarlingMcpServerTrendToolsTests` 13/13, live class 3/3 on a throwaway TimescaleDB container (removed); `McpToolsListBudgetTests` 5/5 with tools/list measured at 185,766 bytes, under the unchanged 186,158 ceiling; `McpToolGuideTests`, `WebReadCancellationPinTests` and the 80-class guard family green except `LivePostgresCollectionHygieneTests` (loads PresentationCore, Mac-only).
## CHANGELOG entry
SECTION: Added
ENTRY: - **More server trends over MCP: latch, spinlock, session stats and collector duration** ([#5170]) - `get_server_trend` now also returns the latch wait and spinlock collision rates per name, the server-wide session counts, and the per-collector run duration trend the desktop charts draw, with a `names` filter for the per-name ones.
REF: [#5170]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5170
* Web Top Queries: the desktop grid's 45 columns, grouped, with a detail=full read for the fields the page lacked (#5169)
Part of #4843
## What users see
The server page's Top Queries grid (both the CPU tab panel and the composite panel) now has the desktop Top Queries columns in the desktop's order, 45 in all (the desktop's Query Plan download column is the separate plan work). The core set shows by default: Database, Query, Module, Execs, Total/Avg CPU, Total/Avg Duration, Total/Avg Reads, Total Spills, Query Hash. Six toggles under the "Columns:" strip turn on the rest: Times (Last Execution, Creation Time, Peak CPU ms/s, Plan Gen, Total CLR), I/O and rows, Extremes, Memory grants, Parallelism, Hashes and handles. Sort, Copy and CSV work on the wider grid; Copy and CSV carry every column. On the hourly tier the fields that tier lacks print a dash.
Query sits right of Database, not behind the two timestamps as on the desktop, because a pinned rule keeps query text next to the anchor. Module at index 2 is a choice of this grid, not a rule the pin states. Last Execution and Creation Time print in the browser's local time.
## What changed
- **Page:** `TOP_QUERY_COLUMNS` (45 keys) plus `TOP_QUERY_GROUPS`, passed to both Top Queries grids. Both ask for `detail: "full"`.
- **Read:** `get_top_queries_by_cpu` gains an optional last parameter `detail` (`summary` default, `full`). `full` adds last_execution_time and creation_time (UTC instants written with the "o" format; the read converts them from the monitored server's clock per row, as every other server-local column on the MCP surface is, and the frame census carries a row for them), min/max physical reads, rows, grant / used grant / ideal grant KB, spills, reserved and used threads, total_clr_ms, plan_generation_num and worker_time_per_second. Fields the store holds no value for are omitted, not written as null. The raw tier only: an hourly-tier read adds nothing, and `precision_note` now says so when `detail=full` was asked and the hourly tier answered. Both aggregates (`TopQueriesSql`, `TopQueriesByHostObjectSql`) select the extra columns and the reader maps them into a `TopQueryDetail` record. The HTTP catalog and dispatch pass `detail`.
- **Size, measured live on a throwaway TimescaleDB, 168 h, top 20:** default call 12,980 bytes, `detail=full` 14,027 bytes (22 groups, only one with the detail columns, short text). A row with every detail field adds 294 bytes, so 20 full rows add about 5.9 KB; the default call is unchanged, and full stays under the 32 KB target unless query text is near its 2,000-char cap on most rows. Neither form has a byte bound: the summary call has no page cut or `truncated` flag for size (query_text is capped at 2,000 characters per row and nothing else bounds the payload), so `full` follows the same rule and adds about 6 KB at 20 rows. A size bound for both forms (a shorter text preview) is a separate follow-up that would change the summary call; it is not part of this PR. Because the default call is untouched, no opt-in was needed beyond `detail`.
- **tools/list:** new line `param get_top_queries_by_cpu.detail 122` in `McpToolsListBudget/DarlingMcpDataTools.txt`. `TotalCeilingBytes` (186,010) did not need to grow (the budget test passes).
- Tests: `DarlingCustomViewsTests` param keys and `WebServerPageRangeTests` read URL now include `detail`.
## Not changed
- `PG_TOP_QUERY_COLUMNS`: a different engine; the PG read does not return these fields.
- Lite and the desktop grid are untouched.
- The Plan column (#5156) is on dev and merged into this branch: both grids pass `[...TOP_QUERY_COLUMNS, planColumn(server)]` plus the groups.
- Peak CPU ms/s is the desktop's `WorkerTimePerSecond`: the max per-collection rate in the window.
## Verified at the head
- New `WebTopQueriesColumnsTests` 8/8: column keys against the read's projection parsed from source, desktop order, group layout (all six groups used, none on by default, core set ungrouped), both grids pass groups and `detail: "full"`, `detail` is an appended optional, both aggregates select the detail columns in the reader's order.
- RED on dev: all 8 fail (the new class run in a worktree of origin/dev).
- New live `TopQueriesDetailLiveTests` 1/1 on a throwaway TimescaleDB container (removed): reader detail values, MCP summary omits them, full carries them and omits nulls, bad `detail` refused, both sizes under 32 KB.
- Also green: `ViewerGridPayloadColumnOrderPinTests` 30/30, `McpToolsListBudgetTests` 5/5, `ServerTabColumnGroupsPinTests` 2/2, `WebServerPageRangeTests` 11/11.
- Full Darling.Tests on the Mac: 22,911 run. After fixing the three failures this PR caused (query-text order, budget txt line, range-test URL), the remaining failures are the Mac-local set (DarlingInstallLocation, Viewer WPF classes, etc.). Five live classes outside the dev-local list failed in the full run and also fail on origin/dev on my rig (TopCpuQueriesTextLive, FinOpsOptimizationTieBreak, StoreMetricsLatestSkipScanLive, ViewerFinOpsIntervalHonestLive, FinOpsOptimizationViewLive); they did not involve this change.
- The guard family and every `.mjs`-harness class were included in that full run.
- At the new head (dev merged in): `WebTopQueriesColumnsTests` 8/8, `TopQueriesDetailLiveTests` 1/1 (non-zero server offset: stored local times come back as UTC; host_object call; a 5-billion physical-reads extreme), `TopQueriesHourlyRoutingLiveTests` 7/7 (a `detail=full` call answered from the hourly tier carries the note and no detail field), `ConsumedTimestampFrameDisciplineTests` 17/17, `ViewerGridPayloadColumnOrderPinTests` 30/30, `McpPayloadContractCensusTests` 68/68, `McpToolsListBudgetTests` 5/5, `WebPlanViewerPageTests` 6/6, the 108-class guard family plus `.mjs`-harness set clean on the Mac except `LivePostgresCollectionHygieneTests` (loads WPF types; dev-local list). Full suite: 22,975 run, 568 failed, every one on the dev-local list or one of the five live classes named above.
- scrubcheck on the diff exits 0.
## CHANGELOG entry
SECTION: Added
ENTRY: - **Web Top Queries shows the desktop grid's columns** ([#5169]) - the server page's Top Queries grid now carries the desktop's 45 columns in six toggle groups (times, I/O and rows, extremes, memory grants, parallelism, hashes and handles), with the core columns on by default. `get_top_queries_by_cpu` gains an optional `detail=full` that adds the grant, thread, extreme, time, CLR and plan-generation fields.
REF: [#5169]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/5169
* get_query_store_top answers long windows from the daily summary, and says the answer is approximate (#5094) (#5168)
get_query_store_top answers long windows from the daily summary, and says the answer is approximate (#5094)
Fixes #5094. The table path used to aggregate every interval row in its window on every call. On a large store a 168-hour read took 6-23 s cold and 2.7 s warm, almost all of it heap reads.
- When the interval table serves the read, the whole UTC days inside the window come from collect.query_store_top_daily (built by #5125), and only the partial days at the edges come from collect.query_store_interval_wide. The read takes the longest contiguous run of built days in the gate's own REPEATABLE READ, READ ONLY snapshot, with ties going to the latest run. With no built day in the window, today's statement runs unchanged (same text, same parameters).
- The summary is approximate by design (Erik's call on #5094): a write into a day after it was built is missed, a row that moves into the next day can count twice until the day's second pass, and a monitored-server clock more than an hour ahead drops rows. In the clean case the answer equals today's exactly. Answers that used the summary carry approximate: true, an approximation_note and summary_days, including the two empty answers, and the web Query Store grid shows the note. The viewer's own Query Store read stays exact.
- Read failures keep #5136's accounting: a fault after the gate chose the table, the built-days read included, is recorded as a raw fallback and answers from raw.
- Tests: QueryStoreTopDailyReadLiveTests (25 facts: equality against today's read on fixed past anchors, edge bounds, run selection, the documented double count, the empty-answer flags and the fault fallback), plus the payload-contract census and the web tab pins.
* query_stats can fetch plan XML in a second target query for only the rows that need it (inert, #5158) (#5173)
query_stats can fetch plan XML in a second target query for only the rows that need it (inert, #5158)
Part of #5158, the first of three PRs. Nothing sets the new switch yet, so no collector's SQL or behavior changes.
- With plan capture on, every query_stats run renders sys.dm_exec_text_query_plan for all of its ~200 rows and ships the XML, although almost all of those plans are already stored. #5158 measured 53.75 GB/h rendered and 28.83 GB/h shipped on one large store, against 1.14 GB/h of distinct plans.
- CollectorContext.DeferPlanXmlFetch (default false) leaves the plan fragments out of BuildQuery and skips the plan ordinals in ReadAsync, through one shared predicate.
- QueryStatsCollector.BuildPlanFetchQuery builds one statement for a list of (plan_handle, start offset, end offset) keys: an inline VALUES list of host-rendered 0x literals, OUTER APPLY sys.dm_exec_text_query_plan, the same size-cap CASE and byte column as the inline fragment, and OPTION(RECOMPILE). It throws when capture is off, the list is empty, or a handle is not 1 to 64 bytes. ReadPlanFetchAsync maps each ordinal to its plan and size.
- Row.KnownPlanDigest and ICollectorRowWriter.PayloadOrDigest (default: Value(content)) let a later writer store a known digest in place of the plan text. WritePayload calls it, and with the default every row writes exactly as before.
- Tests: QueryStatsPlanFetchTests (18) and QueryStatsSqlGoldenTests (4 SHA-256 pins of the full BuildQuery text for Standard and Azure SQL DB, capture on and off, computed on dev and unchanged here).
* Web CPU and Memory tabs chart the CPU scheduler, memory clerk and plan cache trends (#5161)
Part of #4843
**Stacked on #5162 (Perfmon multi-select): this branch contains that PR's commits and uses its new `multi-picker.js` options, so it must land after #5162.**
## What users see
- **CPU tab:** a "CPU Scheduler Trend"…
erikdarlingdata
disabled auto-merge
October 22, 2026 15:23
…-dev-2 Merge main into dev (merge commit) so the 3.11.0 release PR has no conflicts
This was referenced Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Claude posting for Erik Darling.
Promotes dev to main for the 3.11.0 release. Full entries:
docs/changelog/3.11.md(indexed inCHANGELOG.md), dated 2026-10-22. The head isdev.What 3.11.0 is
The release has 4 Important, 114 Added, 27 Changed and 171 Fixed entries.
Headline changes:
get_query_store_aborts, forced-plan checks and load by caller.Before upgrading
The Important section applies:
Darling/tools/provision-roles.sqlafter upgrading.Release checklist status
Directory.Build.props; CHANGELOG and archive spliced through Warn when an Extended Events ring buffer is truncated (#6441, part 1) #6449 (Release 3.11.0: version, changelog, archive #6306). CHANGELOG split, census, verify and roundtrip pass.dotnet build PerformanceMonitor.sln -c Debug -t:Rebuildat the release head: 0 warnings, 0 errors. Statement census release-cut test: pass.Tag
v3.11.0and the GitHub release follow this merge.