Skip to content

Census viewer fan-outs by shape rather than by their join (#3019) - #3023

Merged
erikdarlingdata merged 5 commits into
devfrom
fix/3019-fanout-census-shape
Sep 5, 2026
Merged

erikdarlingdata merged 5 commits into
devfrom
fix/3019-fanout-census-shape

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

Closes #3019. Closing keywords are a no-op on a dev merge, so this is stated rather than relied on.

The defect

EveryViewerFanOut_DeclaresItsWidth iterated Task.WhenAll matches and added offenders only inside that loop. A fan-out that never joins therefore contributed zero iterations and could not be reported however wrong it was — while being the very shape whose discovery took #3007's census from one site to seventeen. The guard's claim was "every joined fan-out", and its name said otherwise.

What changed

A fan-out is N reads in flight together, not a syntax. This project spells that three ways, and the census now recognises all three:

  • Joined — await Task.WhenAll(...). One occurrence makes the member a fan-out.
  • Fire-and-forget — two or more _ = SomethingAsync();. One is the project's ordinary single-flight-guarded refresh (27 such sites, none of them fan-outs); two is a fan-out, because a discarded task has nobody to await it and is still running when the next starts. That is also why an await between two discards does not separate them.
  • Deferred — two or more var t = SomethingAsync(); starts with no await between them. Semantically the joined form with the awaits written one per line.

Each is paired against a declaration in the same member body, which replaces the per-file positional count. That count let declarations launder across unrelated members: MainWindow.xaml.cs carries several declarations against one join, so the declaration that actually paired with that join could be deleted while two unrelated unjoined ones covered for it. Member-body pairing is also immune to the nesting that defeats same-block pairing — CorrelatedTimelineLanesControl declares its width in an outer try and awaits the join in a nested one, and both are the same member.

NoUnmodelledConcurrencyPrimitive_ReachesTheViewer pins Task.WhenAny, Parallel.For/Invoke, Task.Factory and ContinueWith absent from the project. That is what lets the census claim "every" rather than "every one of three spellings": a fan-out spelled with one of those goes red here rather than quietly widening the gap.

The widened census reports four undeclared fan-outs

These are live, not latent. Three spell the fan-out as deferred task starts and each already carried a doc comment stating the reads run concurrently; the fourth fires two unawaited reads.

Member Shape Reads
ViewerServerTab.Blocking.LoadBlockingAsync deferred, three switch cases 3 / 3 / 2
ViewerServerTab.Charts.LoadTempDbAsync deferred 2
ViewerServerTab.RunningJobs.LoadRunningJobsAsync deferred 2
MainWindow.SettingsButton_Click fire-and-forget 2

Each now declares its width. The blocking cases take braces so each declares its own — the branches are mutually exclusive, and one method-level declaration would over-declare for the two branches that read solo, which Of()'s own summary calls the one way to misuse it. The braces are required, not stylistic: a using var directly in an unbraced switch section is CS8647, and the unbraced cases had also been sharing one declaration space. Behaviour is unchanged.

The failure direction was safe throughout: an undeclared fan-out clamps to Math.Clamp(..., 1, ...), the solo floor, which is what these reads already got. So nothing regressed — the reads were priced against a ceiling derived from a read measured alone while running three-wide.

What the census does NOT cover

Stated in the guard's own summary as well, because each gap is real:

  • The declared width is not checked, only that a width is declared.
  • Mutually exclusive branches count as one fan-out, so the rule over-reports rather than under-reports.
  • Within one member, several independent fan-outs share the credit of a single declaration — the positional residual, narrowed from per-file to per-member but not eliminated. Measured, not assumed: deleting two of LoadBlockingAsync's three declarations leaves this pin green, which is why all three are declared by hand rather than left to the guard to demand.
  • Reads reached through a helper the member calls rather than fires are the helper's fan-out, not the caller's.
  • The deferred run is broken by any await between two starts, including one nested inside a lambda rather than sequencing the starts at member level. That direction under-reports — the same direction as the defect itself — and is left because the joined and fire-and-forget rules overlap it and no member is written that way today.
  • The deferred rule keys on the Async suffix. Verified to hold for all 55 Task-returning ViewerDataService members, so it is a convention the rule leans on rather than an assumption about one file.

NoFanOutScope_OutlivesItsJoin remains join-only and now says so. "Outlives its join" needs a join to measure against, and MainWindow.OnRefreshTimerTick deliberately holds its scope to the end of the tick because the visible-tab load below really does contend with reads still in flight — so widening that scan would report a deliberate choice as a defect.

The fire-and-forget shape is unconstrained about its callee, on purpose

Raised in review. The discard scan matches _ = <anything>(...) while the deferred scan requires an Async suffix. Requiring the suffix on discards is not available: OpenPlanTab (private async Task), OnHeatmapDrillDown and PlanViewerController.LoadPlanIntoSubTab are genuinely async without it, so the suffix rule would blind the census to three of the project's fifteen discarded call names — the same silent gap this PR exists to close.

The cost paid instead is a false-positive path: _ = compiles for any non-void call, so two discarded synchronous helpers in one member read as a two-wide fan-out. Nothing in the Viewer does that today (every _ = X(...) site is Task-returning). The shape is now a fixture asserting the classifier's actual behaviour, so it reads as a known cost rather than a surprise, and the trade is argued on the regex itself.

The member walk was missing generic methods, and the two read shapes overlapped

Both raised in review, both fixed rather than documented, because both are the defect this PR exists to close reproduced one level up — a census that cannot see part of what it sweeps, staying green because that part happens to be empty.

Generic methods. The walk required a bare name before the parameter list and only whitespace before the body. A generic method satisfies neither: type parameters sit after the name, constraints sit before the body. Three members were dropped entirely — ViewerSettingsFile.Load<T> and Save<T>, SettingsWindow.TryReadSectionAsync<T>. The two halves are interdependent, measured on the project: admitting the constraint clause alone gains nothing, type parameters alone gain one, both together gain three. Member bodies go 1,231 → 1,234; the fan-out census is unchanged at 21.

The type-parameter group excludes = and newlines deliberately. Allowing them ran the pattern from a field's declared type, through = new Dictionary<...>(comparer), to the collection initializer's brace — reading CollectorSchedulePresets.Presets, a field, as a method whose body is the initializer. A phantom body is worse than a missing one: Owner takes the outermost match, so one could swallow a real member's markers and report a fan-out at a line where no edit makes the build pass. Pinned.

Overlapping shapes. var _ = SomeAsync(); satisfied both the discard and deferred patterns, so one physical call could be counted into two fan-out tallies. A bare _ is now excluded from the deferred name, leaving the call to the discard shape — which is what it is, since nothing holds the task.

One of these pins first passed for the wrong reason and a mutation caught it: the generic fixture was spelled where T : class, new(), whose ) the greedy parameter-list group absorbs, so it matched even with constraint support removed. The real member is paren-free where T : class. The fixture now uses that shape, and the new() layout is pinned separately.

Why unattributed: 0 is coverage rather than silence

The markers are matched against the file and attributed afterwards, not searched for inside each braced body. A marker nothing owns is therefore reported, not skipped. Scoped the other way round, a marker in a shape the walk cannot represent would never be looked at and the count would sit at zero vacuously while covering less.

That decides the expression-bodied join — Task Both() => Task.WhenAll(a, b); is a legal fan-out with no braced body at all, and this project has 378 expression-bodied members against 1,234 braced ones, so the shape is ordinary rather than exotic. None holds a join today, and the census does not depend on that staying true: planting one produces a red build naming its line. Pinned by TheCensus_ReportsAJoinInAMemberWithNoBracedBody, which asserts the reporting path — the join is matched, nothing owns it, so the sweep lists it.

The load-bearing part is demonstrated rather than argued: planting an expression-bodied join and neutering the unattributed reporting leaves the suite green with an undeclared fan-out in the tree, which is exactly the vacuous zero. With reporting intact the same plant is red.

The deferred window was measured from the wrong end

s_deferredRead ends on the call's opening paren, so the window ConcurrentRun scanned for an intervening await began inside the first call and covered the rest of its own argument list. An await in those arguments — var t1 = FooAsync(await Bar()); — broke the run and dropped a real fan-out silently. That is this pin's own failure class inside the detector built for it, and the window was wrong at all 63 deferred sites in the project rather than at an edge case. No site currently puts an await in its arguments, so the census is unchanged at 21 fan-outs and 0 offenders under either window.

DeferredReads now carries the end of the statement, via the paren-balanced EndOfParenthesisedStatement this file already used for the join scan, so a lambda argument cannot terminate it early. The sweep and its fixtures share that helper deliberately: a fixture computing the window differently from the sweep would pin a property the sweep does not have.

Measuring from the statement end introduces an ordering hazard the match end did not have. A deferred read nested inside an earlier statement — a task started in a lambda handed to another started task — puts the previous end past the next start, and the naive slice throws rather than mis-reporting. The window is clamped and the previous end kept monotonic.

Three pins rather than one, because measuring from the wrong end satisfies half the property, and the two halves must fail separately.

Verification

Red-first, one mutation at a time, each restored byte-identical via redproof.sh with a content witness:

Mutation Expected Result
Undeclared deferred-pair fan-out planted in a fresh member — the shape the old guard could never see red red
Undeclared discard-pair fan-out planted in a fresh member red red
Delete the declaration that pairs with MainWindow's only join red red
Delete one of the new blocking-case declarations red red
Plant Task.WhenAny( red, via the primitive pin red
Comment-only: fan-out shapes written as prose green green
Revert both halves of the member-signature widening red red (all 3 generic fixtures)
Revert only the constraint-clause half red red (only the paren-free fixture)
Re-admit = into the type-parameter group red red (field-initializer pin)
Drop the bare-_ exclusion red red (disjointness pin)
Plant an expression-bodied join red red (unattributed, names the line)
Plant it and neuter unattributed reporting green (the hazard) green — the vacuous zero, demonstrated
Flip the expression-bodied pin's assertion red red
Revert the deferred window to the match end red red — only the own-arguments half
Stop honouring the intervening await red red — only the between-statements half
Drop the nesting clamp red red — only the nested pin

The third mutation is the positional-residual evidence: run under the old k-th-join rule it reports green (two declarations precede the only join, and the rule required one), and under the new rule it is red.

The Windows-only suite cannot run on macOS, so the real ViewerCommandTimeoutTests.cs was compiled into a throwaway net10.0 xunit v3 host with <AssemblyName>Darling.Tests</AssemblyName>, staged outside the tree, <Compile Include>-ing the real repo paths alongside the real ViewerCommandDeadlines.cs and ViewerReadFanOut.cs. Counts moved 32 → 42 across the change, so the assembly was not stale. ViewerFleetTimerFanOutPositionTests, ViewerFleetTimerGuardTests, CommandDeadlineScannerAdoptionTests and FleetIdentifierScrubTests were run against the same tree (52 total) because all four read files this diff touches. CI is the arbiter for the rest.

CHANGELOG entry (not committed — for the coordinator to place)

- Viewer fan-out width declarations are now censused by shape rather than by their `Task.WhenAll`, so a fan-out that fires its reads unawaited is covered; four such fan-outs were declaring no width and were reading against a solo-read ceiling ([#3019]).
[#3019]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/3019

EveryViewerFanOut_DeclaresItsWidth iterated Task.WhenAll matches and added
offenders only inside that loop, so a fan-out that never joins contributed zero
iterations and could not be reported. The census now recognises three spellings
of "N reads in flight together" — the join, two or more fire-and-forget
discards, and two or more deferred task starts with no await between them — and
pairs each against a declaration in the same MEMBER BODY.

Member-body pairing replaces the per-file positional count, which let
declarations launder across unrelated members: MainWindow carries three
declarations against one join, so the declaration that actually paired with that
join could be deleted and the two unrelated unjoined ones covered for it.

The widened census reports four members that fan out without declaring a width.
Three spell it as deferred task starts and each already carried a doc comment
saying the reads run concurrently; the fourth fires two unawaited reads.

NoUnmodelledConcurrencyPrimitive_ReachesTheViewer pins WhenAny, Parallel,
Task.Factory and ContinueWith absent, so a fan-out spelled with one of those
goes red here rather than silently widening the gap.
Comment thread Darling/Darling.Tests/ViewerCommandTimeoutTests.cs
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed the diff against dev. This is a C#-only change (test-suite regex classifier + four production call sites in the Darling WPF viewer); no T-SQL, no Lite-side files, and no Lite counterpart exists for ViewerReadFanOut/ViewerCommandDeadlines (it's a Darling-only per-command-deadline mechanism from #2901/#3004), so I don't see a Lite/Darling parity gap here.

Production call sites (MainWindow.xaml.cs, ViewerServerTab.Blocking.cs, .Charts.cs, .RunningJobs.cs): checked each ViewerReadFanOut.Of(n) against the actual number of concurrent reads it guards — all four match (2, 3/3/2, 2, 2 respectively). The Blocking switch cases correctly took their own braces so each declares its own width rather than one declaration covering three mutually-exclusive branches.

Test-suite classifier (EveryViewerFanOut_DeclaresItsWidth and friends): walked through the member-body pairing, the Owner/outermost-body attribution, the ConcurrentRun await-gap logic, and the three new fan-out shapes (joined/fire-and-forget/deferred) against the existing codebase (grepped every _ = X(...) and var t = XAsync(...) site in the Viewer project). Didn't find a case that's mis-classified today. Left one inline note on a latent asymmetry in the fire-and-forget regex (no constraint that the discarded call is actually async) that isn't triggered by anything currently in the tree but could false-positive on a future synchronous discard.

No security, injection, or secrets concerns — this is local static analysis over source text plus in-process AsyncLocal bookkeeping, nothing touching network/file/process boundaries beyond reading .cs files already in the repo.

The discard scan is unconstrained about what it discards while the deferred scan
requires an Async suffix. Requiring the suffix on discards is not available:
OpenPlanTab, OnHeatmapDrillDown and PlanViewerController.LoadPlanIntoSubTab are
async without it, so the suffix rule would blind the census to three of the
project's fifteen discarded call names.

The cost of leaving it unconstrained is that two discarded synchronous helpers
in one member read as a two-wide fan-out. That shape is now a fixture asserting
the classifier's actual behaviour, so it reads as a known cost rather than a
surprise to whoever first writes one.

Also records the remaining under-reporting gap: an await nested in a lambda
between two deferred starts breaks the run.
Comment thread Darling/Darling.Tests/ViewerCommandTimeoutTests.cs
Comment thread Darling/Darling.Tests/ViewerCommandTimeoutTests.cs
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. This is Darling-only C# test/lint infrastructure plus four production call sites (MainWindow.xaml.cs, ViewerServerTab.Blocking.cs, .Charts.cs, .RunningJobs.cs) — ViewerReadFanOut doesn't exist in Lite, so there's no Lite/Darling parity concern here. No T-SQL touched.

Verified against the actual production changes: the four newly-declared widths (Of(3)/Of(3)/Of(2) in LoadBlockingAsync's three switch cases, Of(2) in LoadTempDbAsync, Of(2) in LoadRunningJobsAsync, Of(2) in SettingsButton_Click) all match the actual number of concurrent unawaited reads at each site, and each using var scope is declared before the first read it covers. The AsyncLocal-based ViewerReadFanOut.Scope mechanism means the SettingsButton_Click case (a synchronous void handler whose using disposes essentially immediately) still works correctly, since the width is captured into each fired task's execution context at creation time rather than depending on the declaring frame staying alive.

Traced through the new classifier logic (MemberBodies/Owner/ConcurrentRun, the member-body pairing, the three-shape census) against the regexes and it holds together for the shapes exercised by TheFanOutCensus_RecognisesEachShape_AndOnlyThose. Left two inline notes on real (if currently dormant) gaps in the new regexes that aren't in the PR's own "what this does NOT cover" list:

  • s_memberSignature doesn't tolerate a generic where T : ... constraint clause between the parameter list and the opening brace, so two real methods in this project (ViewerSettingsFile.Load<T>/Save<T>, SettingsWindow.TryReadSectionAsync<T>) are invisible to the member walk. Neither contains a fan-out marker today, so it's silent for now, but it fails loud (unattributed assertion) rather than being caught by name if one of them ever grows one.
  • s_fireAndForget and s_deferredRead overlap on var _ = SomeAsync(); — unexploited in the current tree, but the two shapes aren't disjoint the way the PR's stated fire-and-forget/deferred asymmetry implies.

Neither blocks the fix for #3019 itself, which is correctly targeted (member-body pairing replacing the per-file positional count, plus the two previously-uncensused shapes).

The member walk required only whitespace between the parameter list and the
body, and a bare name before it. A generic method satisfies neither: its type
parameters sit after the name and its constraints sit before the body. Three
members were dropped from the walk entirely -- ViewerSettingsFile.Load<T> and
Save<T>, SettingsWindow.TryReadSectionAsync<T>. None holds a fan-out marker, so
the census stayed green while seeing less than it claimed, and the first one to
grow a marker would have tripped the unattributed assertion instead of being
reported as an offender.

The two halves are interdependent, measured on the project: admitting the
constraint clause alone gains nothing, type parameters alone gain one member,
and both together gain three.

The type-parameter group excludes = and newlines deliberately. Allowing them ran
the pattern from a field's declared type through its initializer to the
collection initializer's brace, reading CollectorSchedulePresets.Presets as a
method whose body is the initializer. A phantom body is worse than a missing
one, since Owner takes the outermost match and could attribute a real member's
markers to something that can never declare a width. Both properties are pinned.

The generic fixture spells its constraint without parentheses on purpose:
where T : class, new() contains a ), the parameter-list group is greedy, and it
absorbs the whole clause -- so a fixture written that way passes with constraint
support removed. That layout is pinned separately instead.

Separately, `var _ = SomeAsync();` satisfied both read shapes, so one call could
be counted into two fan-out tallies. A bare _ is now excluded from the deferred
name, leaving the call to the discard shape, which is what it is: nothing holds
the task.

Member bodies go 1,231 to 1,234; the fan-out census is unchanged at 21.
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed the diff (test-only classifier rewrite in ViewerCommandTimeoutTests.cs + four production sites declaring their fan-out width). Notes:

Correctness — Walked the three regexes (s_fireAndForget, s_deferredRead, s_memberSignature) and the Owner/ConcurrentRun/MemberBodies helpers by hand against the edge cases the PR itself calls out (discard vs. deferred disjointness, nested try blocks, generic methods with where clauses, field collection initializers). All check out, and they're already backed by targeted [Theory] fixtures (TheFanOutCensus_RecognisesEachShape_AndOnlyThose, TheMemberWalk_SeesAGenericMethod, TheDiscardAndDeferredShapes_StayDisjoint, TheMemberWalk_DoesNotReadAFieldInitializerAsAMember) that exercise exactly the failure modes described in the doc comments. Didn't find a case those miss.

Cross-checked the four newly-declared widths against the actual task counts in each member:

  • LoadBlockingAsync Trends/Stats cases → Of(3) / 3 tasks each ✓
  • LoadBlockingAsync Current Waits case → Of(2) / 2 tasks ✓
  • LoadTempDbAsync → Of(2) / 2 tasks ✓
  • LoadRunningJobsAsync → Of(2) / 2 tasks ✓
  • MainWindow.SettingsButton_Click → Of(2) / 2 discards (RefreshAlertToastSettingsFromStoreAsync + the conditional LoadVisibleTabAsync)

One minor/non-blocking note on that last one: the Of(2) declaration is unconditional, but the second discard (LoadVisibleTabAsync) only fires inside if (CurrentDisplayMode != previousMode). When the mode doesn't change, the lone RefreshAlertToastSettingsFromStoreAsync read is priced as width-2 instead of width-1. Per ViewerReadFanOut's own contract this is the safe direction (over-declaring only lengthens a deadline, never shortens one), so I'm not flagging it as a defect — just noting it in case a tighter placement was intended.

Lite/Darling parity — No parity concern. ViewerReadFanOut/ViewerCommandDeadlines is Darling-only network-deadline machinery that has no Lite counterpart (Lite reads a local embedded DuckDB file, not a pooled remote store), and this PR doesn't touch Lite at all.

Security / T-SQL style — N/A, no SQL or external input handling in this diff.

Performance — None of the production changes alter behavior; they only widen an AsyncLocal scope around reads that were already firing concurrently, and the guard clamps into ManagedMaxPoolSize so it can't inflate the deadline unboundedly.

No blocking issues found.

The markers are matched against the file and attributed afterwards, so a marker
no member body owns is reported rather than skipped. Scoped the other way --
searched within each braced body -- a marker in a shape the walk cannot
represent would never be looked at, and the unattributed count would sit at zero
vacuously while covering less.

That distinction decides an expression-bodied join, which is a legal fan-out
site with no braced body at all: this project has 378 expression-bodied members
against 1,234 braced ones. None holds a join today, and the census does not
depend on that staying true.

The new pin asserts the reporting path rather than coverage: the join is
matched, nothing owns it, so the sweep lists it. The guard's summary now says
why the tree-wide scan is what makes its unattributed assertion mean anything.
Comment thread Darling/Darling.Tests/ViewerCommandTimeoutTests.cs Outdated
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. This is entirely test-tooling + Darling-only production wiring (no T-SQL, no Lite files touched), and ViewerReadFanOut has no Lite counterpart, so there's no Lite/Darling parity gap to flag here.

Correctness

  • Traced the new regexes (s_fireAndForget, s_deferredRead, s_memberSignature, s_unmodelledConcurrency) and the MemberBodies/Owner/ConcurrentRun helpers against the pinned [Theory] cases by hand — they check out, including the disjointness between the discard and deferred shapes and the outermost-body attribution for nested try blocks.
  • Left one inline note on ConcurrentRun: the intervening-await window is measured from right after the opening paren of the first deferred call (not the end of its statement), so a deferred call whose own argument list contains a nested await could be misread as "sequential" and silently drop a real fan-out. No such shape exists in the current ViewerSources() today (grepped for it), so this isn't live, but it's a different gap than the ones already documented and might be worth a one-line callout or a fix to where End is measured.
  • The four newly-declared production fan-outs (LoadBlockingAsync's three switch cases, LoadTempDbAsync, LoadRunningJobsAsync, SettingsButton_Click) all match the actual read counts at each site, and the new braces around the Blocking switch cases are load-bearing (needed for using var per-case scoping, not just style) — correct.

Security / performance: no external input, no SQL, no perf-sensitive path — this only runs at test time over local source files.

Solid, unusually well-verified PR (the mutation-testing table in the description is a good sign). Nothing blocking.

s_deferredRead ends on the call's opening paren, so the window ConcurrentRun
scanned for an intervening await began inside the first call and covered the
rest of its own argument list. An await in those arguments --
var t1 = FooAsync(await Bar()); -- broke the run and dropped a real fan-out
silently, which is this pin's own failure class inside the detector built for
it. The window was wrong at all 63 deferred sites in the project, not at an edge
case; no site currently puts an await in its arguments, so the census is
unchanged at 21 fan-outs and 0 offenders either way.

DeferredReads now carries the end of the STATEMENT, via the paren-balanced
EndOfParenthesisedStatement this file already used for the join scan, so a
lambda argument cannot terminate it early. The sweep and its fixtures share that
helper deliberately: a fixture computing the window differently from the sweep
would pin a property the sweep does not have.

Measuring from the statement end introduces an ordering hazard the match end did
not have. A deferred read nested inside an earlier statement -- a task started
in a lambda handed to another started task -- puts the previous end PAST the next
start, and the naive slice throws rather than mis-reporting. The window is
clamped and the previous end kept monotonic. No site is written that way today.

Three pins rather than one, because measuring from the wrong end satisfies half
the property: an await inside the first call's arguments must still count as a
fan-out, an await genuinely between the two statements must still break the run,
and the nested case must return at all.
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewed. This is a Darling-only, test-infra-heavy change (622 of 639 lines are in ViewerCommandTimeoutTests.cs); no T-SQL, no Lite files touched.

Lite/Darling parity: No concern. ViewerReadFanOut is a Darling-only concurrency-accounting abstraction (confirmed via repo-wide grep — it doesn't exist anywhere under Lite/), so there's no counterpart to keep in sync.

Regex/census logic: Traced the three new shape detectors (s_fireAndForget, s_deferredRead, s_memberSignature) and the ConcurrentRun/Owner/MemberBodies helpers by hand against several edge cases not explicitly called out in the PR description:

  • _ = await SomethingAsync(); correctly does not match s_fireAndForget (the discard anchor requires an identifier immediately after =, and await followed by a space fails the subsequent \s*\( check) — good, since that's a synchronous discard, not fire-and-forget.
  • protected internal members still parse correctly under s_memberSignature (the lazy return-type group absorbs the extra modifier since internal isn't in the explicit modifier alternation).
  • s_unmodelledConcurrency's Parallel\s*\.\s*(?:For|Invoke) also incidentally catches Parallel.ForEach/ForEachAsync since there's no trailing anchor — a happy accident, not a gap.
  • Cross-checked the "63 deferred sites" claim in the doc comments with an independent grep over PerformanceMonitor.Darling.Viewer — matches exactly.

No correctness bugs found in the detection logic.

Production one-liners (the actual behavior fix):

  • ViewerServerTab.Blocking.cs: switch-case braces are balanced correctly and each using var readFanOut is scoped to its own case (disposed at break), matching the declared widths (3/3/2) to the actual concurrent read counts per branch.
  • ViewerServerTab.Charts.cs / RunningJobs.cs: Of(2) matches the two concurrent deferred reads in each method.
  • MainWindow.xaml.cs SettingsButton_Click: Of(2) covers two discards (RefreshAlertToastSettingsFromStoreAsync unconditional, LoadVisibleTabAsync conditional on display-mode change). When the mode doesn't change, only one discard actually fires but the width is still declared as 2 — this over-declares in the safe direction (a longer allowed deadline than needed) and is explicitly the accepted tradeoff the PR's own doc comments describe for conditional/mutually-exclusive branches, so not a bug.

No security, injection, or missing-index concerns — nothing here touches SQL or external input.

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