Skip to content

get_spinlock_stats moves reading guidance off tools/list (#3898) - #4127

Merged
erikdarlingdata merged 6 commits into
devfrom
feature/3898-content-spinlock
Sep 24, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
feature/3898-content-spinlock

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Refs #3898.

Why

get_spinlock_stats is a twin tool whose two served descriptions differed a lot: Darling's was already terse
at 597 characters, but Lite's ran to 1,306. Wave E converts it: the reading guidance moves off tools/list into
get_tool_guide, and the served head stays true on both products.

Lane e5 had one extra constraint. The head plus the 31-character pointer had to stay at or under 597 characters,
so Darling's tools/list entry does not grow by even one byte.

What changes

  • Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpLatchSpinlockTools.cs: get_spinlock_stats
    converted. Every original sentence moved to the tail, verbatim.
  • Lite/Mcp/McpLatchSpinlockTools.cs: same tool, same head (byte-identical), Lite's own original sentences in
    its own tail.
  • Darling/Darling.Tests/McpToolsListBudget/DarlingMcpLatchSpinlockTools.txt and
    Lite.Tests/McpToolsListBudget/McpLatchSpinlockTools.txt: only the get_spinlock_stats lines changed. Lane
    e4's get_latch_stats lines in the same two files are untouched.
  • New pins: Darling/Darling.Tests/McpToolGuideHeads.Spinlock.cs and Lite.Tests/McpToolGuideHeads.Spinlock.cs.

The two products do not do the same thing here. The head says so in one clause instead of picking either
product's shape (D6). Darling sums every collection in hours_back and ranks the top N by that total. Lite
always returns the single newest snapshot found within hours_back: cumulative counters plus the last
interval's deltas, bounded by limit.

Both products still derive interval_seconds and the two per-second rates from the latest interval only. Both
go null, never a quiet 0, when that interval is unknowable. The window totals (Darling) or cumulative counters
(Lite) stand regardless.

Served characters, before and after

Tool Darling before Darling after Lite before Lite after
get_spinlock_stats 597 595 1,306 595

Both heads are byte-identical at 595 served characters (a 564-character head plus the 31-character pointer),
2 characters under the lane's 597 cap.

Corrections

None. Every fact in the original text checked out against DarlingLatchSpinlockReader.GetSpinlockStatsTopNAsync
(the window-sum SQL) and LocalDataService.GetSpinlockStatsSnapshotAsync (the latest-snapshot SQL), plus the
PerSecond/KnownInterval helpers in McpLatchSpinlockTools.cs.

D4 removals

None. Neither product's wire text carried an issue reference or an anecdote to begin with. This family was
already written in the terse, code-cited style #3898 asks for.

Re-pointed pins

None. Every existing test that mentions get_spinlock_stats passed unmodified: DarlingMcpLatchSpinlockToolsTests,
McpPageContractTests on both products, McpLatestSnapshotStampTests on both products, and
CrossAppMcpToolInventoryPinTests. None of them pinned a sentence by its position in the old, single-block
description.

D9 traps

  • Q: get_spinlock_stats answers status unavailable with no spinlocks array. Is that proof the server has
    zero spinlock contention right now?
    Correct: No. unavailable means nothing was collected in the requested window, or the collector cannot
    run on this engine at all, reported as not_collected first. It says nothing about whether contention exists.
    Prevents it: No rows: unavailable (or not_collected first).
  • Q: Are Darling's total_delta_collisions and Lite's delta_collisions the same kind of number?
    Correct: No. Darling's is a sum over every collection in hours_back. Lite's is just the newest
    collection's own last-interval delta.
    Prevents it: Darling sums every collection in hours_back, top N by total collisions; Lite returns only the latest snapshot within hours_back, cumulative counters plus last-interval deltas
  • Q: Does Lite's get_spinlock_stats aggregate contention over the whole hours_back window, the way
    Darling's does?
    Correct: No. Lite always serves one moment: the newest snapshot found within hours_back, never a window
    sum.
    Prevents it: Lite returns only the latest snapshot within hours_back
  • Q: On a restart row, does collisions_per_second read 0.00?
    Correct: No, it reads null. A 0.00 reads as a measured, quiet rate. null reads as an unmeasured
    interval, not a quiet one.
    Prevents it: null - never 0 - when unknowable (restart/first sample)
  • Q: On that same restart row, does Darling's window total for the spinlock, or Lite's cumulative counter,
    also go null or drop that collection?
    Correct: No. Only interval_seconds and the two per-second rates go null on that row. The window sums and
    the cumulative counters beside them are unaffected.
    Prevents it: totals/counters still stand.
  • Q: Does Darling's get_spinlock_stats have a truncated flag, like Lite's?
    Correct: No. Darling's top returns up to that many rows. There is no signal for how many more exist
    beyond it.
    Prevents it: bounded by limit (truncated flags more) is written inside the sentence that starts "Lite
    returns," not as a claim true of both products.
  • Q: Lite's truncated is true. Does that mean more spinlocks existed somewhere across the whole
    hours_back window than are shown?
    Correct: No. It is a single-snapshot page bound. The one latest collection held more rows than limit.
    That has nothing to do with how far back hours_back reaches.
    Prevents it: Lite returns only the latest snapshot within hours_back ... bounded by limit (truncated flags more)
  • Q: Do interval_seconds and the per-second rates ever reflect an average across the whole window, even on
    Darling, where the other fields are window sums?
    Correct: No. On both products they always come from the single latest interval.
    Prevents it: interval_seconds and the rates reflect the latest interval, never a window total
  • Q: High spinlock contention is CPU-bound, so does it also show up as high wait stats?
    Correct: No, the opposite is true. This is exactly the contention wait stats cannot see, which is why the
    tool exists as a separate read.
    Prevents it: High contention is CPU-bound, not in wait stats.
  • Q: Does top/limit rank spinlocks by the latest interval's collisions on both products?
    Correct: No. Darling ranks by the total collisions summed over hours_back. Only Lite's default order uses
    the latest interval's delta.
    Prevents it: Darling sums every collection in hours_back, top N by total collisions
  • Q: Does hours_back mean the same thing on both products?
    Correct: Not quite. On Darling it is the period that gets summed. On Lite it is only how far back to
    search for the newest snapshot. Nothing across it gets aggregated.
    Prevents it: Darling sums every collection in hours_back ... Lite returns only the latest snapshot within hours_back
  • Q: Is a not_collected status the same thing as unavailable: no data in the window?
    Correct: No. not_collected means the collector cannot run on this server's engine or edition at all, and
    that is checked first. unavailable means this engine supports the collector, and nothing landed in this
    particular window.
    Prevents it: No rows: unavailable (or not_collected first). The word "first" marks the check order.

Coordinator verification

D9 verification pass by a second lane, on the same branch. git fetch origin dev then git merge --no-edit origin/dev merged clean (merge commit 4ca0c599). It picked up 5 dev commits, including #4111, #4112, #4115
and #4116. No conflicts, and no neighboring McpToolsListBudget line was touched.

Size and identity

desc.py get_spinlock_stats after the merge showed Darling head 553, served 584, and Lite the same. After the
coordinator's lesson 5 fix below, both are head 564, served 595, still byte-identical and under the 597 cap. dropcheck.py origin/dev HEAD get_spinlock_stats shows Darling
sentences=4 missing=0 and Lite sentences=6 missing=0. paramcheck.py origin/dev HEAD get_spinlock_stats
printed no findings.

Every head/tail fact, re-checked against the tool body and its reader SQL on both products

This is this lane's own independent read, separate from the original lane's citations above.

  • Darling sums every collection in hours_back and ranks the top N by that total. Confirmed in
    DarlingLatchSpinlockReader.cs's SpinlockStatsTopNSql: the agg CTE sums delta_collisions and friends
    (lines 189-198), then ORDER BY a.total_delta_collisions DESC LIMIT $4 (lines 221-224). Also
    DarlingMcpLatchSpinlockTools.cs:133-189.
  • Lite reads only the newest snapshot in the window. It never aggregates over the window. Confirmed in
    LocalDataService.LatchSpinlock.cs:263-295: a latest CTE takes MAX(collection_time), then the main query
    filters WHERE collection_time = (SELECT mx FROM latest). Also McpLatchSpinlockTools.cs:120-179, where
    captured_at reads rows[0].CollectionTime. This is the same asymmetric shape as get_latch_stats moves reading guidance off tools/list (#3898) #4119's latch twin, the
    brief's own comparison case.
  • interval_seconds and the two per-second rates always come from the latest interval on both products, never
    a window total. Darling's latest CTE (DarlingLatchSpinlockReader.cs:200-211) is a separate DISTINCT ON (spinlock_name) ... ORDER BY collection_time DESC, apart from the agg CTE that builds the totals. Lite's
    PerSecond/KnownInterval helpers (McpLatchSpinlockTools.cs:38-46) read only the newest row's
    sample_interval_seconds.
  • Null, never 0, when the interval is unknowable, with the totals and counters unaffected. Darling's
    NULLIF(sample_interval_seconds, 0) (DarlingLatchSpinlockReader.cs:180-183) feeds a CASE WHEN interval_seconds > 0 gate that only touches the rate columns (lines 204-208). The restart row's own
    delta_collisions value is 0. It sums into the total the same as any other row, so the total itself is
    never nulled. Lite's DeltaSeriesShaping.ReadableDelta (PerformanceMonitor.Common/DeltaSeriesShaping.cs:320-321)
    nulls the delta only when intervalSeconds == 0. It leaves the delta standing, as a real value, when the
    interval is NULL instead, on a row from before the interval column existed. That matches the tail's own
    extra clause about a row "collected before the interval was stored."
  • Paging. Darling has no truncated field and no signal for rows beyond top (the response shape at
    DarlingMcpLatchSpinlockTools.cs:177-183 has no such key). Lite fetches limit + 1 rows and sets
    truncated = rows.Count > limit (McpLatchSpinlockTools.cs:141,146), ordered delta_collisions DESC, collisions DESC (LocalDataService.LatchSpinlock.cs:294). The head's "bounded by limit (truncated flags
    more)" clause sits inside the Lite half of the sentence only. It is not claimed for Darling.
  • No rows: not_collected is checked first, with unavailable as the fallback, on both products.
    DarlingEngineCapability.cs:62-82 returns Status("not_collected", ...) at line 81, and
    McpEngineCapability.cs:43-68 does the same at line 68. Both tool bodies call it before falling back to
    McpHelpers.Status("unavailable", ...).
  • captured_at and age_seconds on Lite. age_seconds is windowEnd - capturedAt, clamped at zero
    (McpLatestSnapshotStamp.cs:26-27). That is its distance from the window's end, not from wall-clock now.
  • The LATEST IS A TIME phrase: the verifying lane left it in Lite's tail only, reasoning that a byte-identical
    head cannot carry a Lite-only fact. The coordinator overruled that. Wave E lesson 5 puts the phrase in the head
    of a latest-snapshot read. A twin head can name one product's behavior: get_latch_stats (get_latch_stats moves reading guidance off tools/list (#3898) #4119, merged)
    already says "Lite: LATEST IS A TIME, only the newest snapshot within hours_back". See the coordinator fix
    below. The generic census
    Lite.Tests/McpLatestSnapshotStampTests.cs:294-304 checks the whole description, head and tail
    together, so it passed either way. Lesson 5 asks for the head.

Folklore

Traced "High spinlock contention indicates CPU-bound internal contention that doesn't appear in wait stats" to
its origin at deprecated/Dashboard/Mcp/McpLatchSpinlockTools.cs:77. Darling's tail carries it verbatim, and
Lite's tail carries the same claim with light rewording. Checked it against two Microsoft Learn pages.
sys.dm_os_spinlock_stats
says contention can cause "significant CPU utilization." The
Whitepaper: Diagnose & Resolve Spinlock Contention
says "the symptom primarily associated with spinlock contention is high CPU consumption." Both support the
"CPU-bound" half of the claim. The whitepaper's diagnostic-tools table also lists a wait-statistics row. Starting
with SQL Server 2025 (17.x), sys.dm_os_wait_stats and sys.dm_exec_session_wait_stats can show spinlock time
under the SPINLOCK_EXT wait type, gated behind trace flag 8134.

That table row is a documented, narrow exception to the "doesn't appear in wait stats" claim. On 17.x, with a non-default
trace flag turned on, spinlock time does surface as a wait type. By default, and on every version before 17.x,
it still does not. The brief's own standard is "a claim that is only imprecise, not false, stays." This
sentence is not the kind of flatly wrong attribution #4119 corrected for ACCESS_METHODS_DATASET_PARENT/TempDB.
It is a true general-case statement with one opt-in, version-gated exception. Judged imprecise rather than
false on that standard, so the head, both tails and the deprecated Dashboard copy are all unchanged. Recording
the exception here for the coordinator's own call.

No corrections, no re-pointed pins

Checked the changelog buffer entry's size claims (597 and 1,306 characters before, 584 after, on both products)
against dropcheck.py's own counts. They already matched, so the file is unchanged.

Coordinator fix (lesson 5)

  • Head fact fixed: 1. "Lite returns only the latest snapshot within hours_back" now reads "Lite: LATEST IS A
    TIME, only the newest snapshot within hours_back". Lite's read is LocalDataService.LatchSpinlock.cs, which
    takes only the newest snapshot. Head 553 to 564, served 584 to 595, on both products. The head pins, the two
    budget lines and the changelog buffer entry now say 595.
  • The branch also merged dev again after get_latch_stats moves reading guidance off tools/list (#3898) #4119 landed, since that PR touched the same four files. The merge was
    clean.
  • D9 round 1 (haiku, 16 questions: the 12 above plus 4 new ones on the snapshot, the rates, the counters and
    wait stats): 0 misreads. Q2, Q6, Q7, Q11, Q12 and Q13 were answered "can't tell". Every other answer was
    correct.
  • Targeted tests after the fix: Darling 330 total, 0 failed, 11 skipped. Lite 115 total, 0 failed. The classes
    were the list below plus *Spinlock* and *LatchSpinlock*.

Targeted tests, re-run on the merged tree

Darling.Tests.exe and Lite.Tests.exe, filtered by every class this PR's files touch. Also every class
git grep -l get_spinlock_stats finds in the test projects: *McpToolGuide*, *McpToolsListBudget*,
*McpDescription*, *McpZeroIsAMeasurement*, *McpPayloadContractCensus*, *MeasurementContractCensus*,
*McpPageContract*, DarlingMcpLatchSpinlockToolsSurfaceAndSqlTests,
DarlingMcpLatchSpinlockToolsLivePostgresTests, DarlingMcpMemoryGrantToolsSurfaceAndSqlTests,
DarlingMcpMemoryGrantToolsLivePostgresTests, McpLatestSnapshotStampTests.

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: Build succeeded, 0 Warning(s), 0 Error(s).
  • dotnet build Lite.Tests/Lite.Tests.csproj -c Debug: Build succeeded, 0 Warning(s), 0 Error(s).
  • Targeted tests, both products (McpToolGuide*, McpToolsListBudget*, McpDescription*,
    McpZeroIsAMeasurement*, McpPayloadContractCensus*, DarlingMcpLatchSpinlockToolsTests,
    McpPageContractTests, McpLatestSnapshotStampTests, CrossAppMcpToolInventoryPinTests,
    SpinlockStatsCollectorDefinitionTests): Darling 293 total, 0 failed, 4 skipped (live-Postgres, no rig).
    Lite 110 total, 0 failed.
  • Red-watch: broke the new "totals/counters still stand." fact in the head, watched
    McpToolGuideHeadsSpinlockTests fail with the exact Assert.Contains miss, restored it, watched it pass
    again.
  • python dropcheck.py origin/dev HEAD get_spinlock_stats: Darling sentences=4 missing=0, Lite
    sentences=6 missing=0.
  • git merge origin/dev brought in get_blocking/get_pg_replication_slots move reading guidance off tools/list (#3898) #4108, get_query_store_regressions moves reading guidance off tools/list (#3898) #4109, DarlingInstallLocationTests: the #4052 checks pass when run non-elevated #4111, and get_plan_corrections moves reading guidance off tools/list (#3898) #4105. There were no conflicts, and none of
    those PRs touch this lane's files.
  • Full suite, Darling (Darling.Tests.exe, no filter): 13,337 total, 0 failed, 636 skipped, 1 not run.
    The 636 skips are the usual live-Postgres/pg-runtime classes with no rig. The 1 "not run" is unchanged
    across two separate runs of the full suite in this worktree. It is unrelated to this PR's files and was
    not investigated further, per the brief's token-economy guidance. The two DarlingInstallLocationTests
    failures the brief calls out as known-until-DarlingInstallLocationTests: the #4052 checks pass when run non-elevated #4111 do not reproduce here. DarlingInstallLocationTests: the #4052 checks pass when run non-elevated #4111 merged into dev before
    this branch's git merge origin/dev, and both tests now pass.
  • Full suite, Lite (Lite.Tests.exe, no filter): 5,270 total, 0 failed on the final run. An earlier run
    (before the last content commit) showed 1 failure in
    AnalysisPassTokenThreadingTests.TheReadLockWaitIsAbandonableWhileAWriterHoldsIt, a lock-timing test with
    no connection to this PR's files. Re-run alone, it passed. The final full run then passed clean too,
    confirming a pre-existing flake, not a regression from this change.
  • Plain-English checker on this PR body: run twice. The first pass found 50 hits, and the real ones got
    fixed. The final pass found 26: pr_body_3898-content-spinlock.md: 26 hit(s), doc mode. 24 are the exempt
    **Correct:**/**Prevents it:** bold-lead-ins, and 2 are a semicolon and a word count inside one
    **Prevents it:** code span, which quotes the head's exact text (also exempt) and cannot be reworded
    without misquoting it.

Handoff

None. get_spinlock_stats was this lane's only tool, and it converted cleanly on both products within the
597-character cap.

erikdarlingdata and others added 6 commits September 23, 2026 22:58
Converts the get_spinlock_stats twin: a 597-char head (pointer included)
that states both products' shape in one clause (Darling sums the window,
Lite serves the latest snapshot) so Darling's already-terse original does
not grow. Every original sentence rides each product's own tail verbatim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
…unters standing (#3898)

The window-summed totals (Darling) and the cumulative counters (Lite) are
unaffected when the latest interval is unknowable and its own rates go
null; the head said the rates go null but not that the rest of the row
stands, so state it, and raise the budget lines to the new 584-char
served length (the lane's 597-char cap still holds).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
Wave E lesson 5: a latest-snapshot read keeps the catalog phrase in its head. Lite's
get_spinlock_stats reads only the newest snapshot within hours_back
(LocalDataService.LatchSpinlock.cs), so its clause now reads "Lite: LATEST IS A TIME,
only the newest snapshot within hours_back", matching get_latch_stats. Head 564, served
595 on both products, under the 597 cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 04:29
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 24, 2026 04:29
@erikdarlingdata
erikdarlingdata merged commit 5423369 into dev Sep 24, 2026
26 of 30 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-content-spinlock branch September 24, 2026 04:56
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
* Add 48 buffered CHANGELOG entries in one splice

Adds 48 entries and 48 link refs (#4057, #4077, #4079, #4081, #4082, #4083, #4084, #4085, #4086, #4087, #4088, #4089, #4090, #4091, #4092, #4093, #4095, #4096, #4099, #4100, #4101, #4103, #4105, #4106, #4107, #4108, #4109, #4110, #4111, #4113, #4114, #4115, #4116, #4117, #4118, #4119, #4120, #4121, #4122, #4123, #4124, #4125, #4126, #4127, #4131, #4133, #4136, #4141). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran.

Entries found under a bare section line (none unless the old script ran again) move into the matching ### section, below the new entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

* Changelog splice: add #4137's entry, which merged before the splice but was missed

#4137 (pg_plan_capture reads csvlog) merged at 05:57Z with a CHANGELOG entry section in its
body. It was neither spliced nor listed as needing no entry. Its entry goes under Fixed, right
after #4136's, and #4136's last sentence now points to it instead of saying plan capture is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant