Add lifetime stress test harness + scheduled soak pipeline - #11830
Open
Protik Biswas (protikbiswas100) wants to merge 29 commits into
Open
Protik Biswas (protikbiswas100) wants to merge 29 commits into
Protik Biswas (protikbiswas100) wants to merge 29 commits into
Conversation
Reintroduces the object-lifetime coverage XAML lost when the old Win8-era lifetime tests were dropped. Adds an isolated TAEF suite that repeatedly creates/loads/unloads/reparents controls (plus a dedicated ItemsRepeater realization/recycling scenario) while aggressively forcing GC + finalizers each iteration, so dangling native peers fault promptly instead of eventually. - LifetimeStressTests.cs: gate by default (auto-runs as an isolated Helix work item on every DevTestSuite pass); soak-capable via WINUI_LIFETIME_STRESS_* environment variables. - WinUI-LifetimeStress.yml: scheduled pipeline that builds and runs the suite in soak mode. - docs/testing/lifetime-stress-tests.md: rationale, CI behavior, usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Protik Biswas (protikbiswas100)
requested a review
from a team
as a code owner
September 9, 2026 08:22
…gate
The LifetimeStressTests class was already being picked up by the WinUI-GitHub-PR
(DevTestSuite) test pass implicitly: MUXControlsTestApp sets Classification=Integration
module-wide via ApiTestAssemblyHandling.AssemblyInitialize, and Helix\GenerateHelixWorkItems.ps1
emits a dedicated per-TestSuite work item ("*-LifetimeStressTestSuite") into
RunTestsInHelix-MUXControlsApiTests.proj, which the run-test job executes without any
suite filter.
Declare [TestProperty("Classification", "Integration")] on the class explicitly so that
participation in the per-PR gate is self-documenting and robust to future changes in the
module-level defaults, matching the convention used by the InteractionTests classes. The
value is identical to the inherited module-level property, so there is no behavior change
beyond making the intent explicit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b2bb4a27-c21b-4b24-8fcd-e67911217778
The StressControlCreateLoadUnloadCollect scenario used failOnLeak:true, which emitted a failed test result on any residual reference. That failed the Run Tests stage's Publish Test Results step and blocked the whole pipeline on a soft/flaky leak signal. Log leaks as warnings instead so the suite reports lifetime concerns without gating; a real lifetime crash still fails its isolated work item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ort) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses review feedback to cover all controls in StressControlCreateLoadUnloadCollect. Expands CreateControlSet from a small sample to essentially every Microsoft.UI.Xaml.Controls control that is cheaply constructible without a live window/parent/service (excluding WebView2, MapControl, InkToolbar and flyout-only types, which need special hosting; ItemsRepeater has its own scenario). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This scenario crashes the TAEF test host (TE.ProcessHost.exe) with a stowed
exception (0xC000027B) in combase.dll during the realize/recycle churn, which
surfaces as an RPC failure in the Run Tests stage and unconditionally fails the
PR pipeline. A host crash cannot be downgraded to a warning the way a
WeakReference leak can (the process fail-fasts before any managed result is
reported), so mark it [TestProperty("Ignore","True")] following the repo's
existing quarantine convention. Re-enable once the underlying ItemsRepeater
native-peer lifetime bug it exposes is root-caused and fixed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rage
Runner timeout (addresses review feedback):
In soak mode each lifetime scenario loops on a wall-clock budget
(WINUI_LIFETIME_STRESS_MINUTES, default 10 min) but RunHelixWorkItem.ps1
hardcoded a 5-minute per-test TAEF timeout (/testtimeout:0:05), so TAEF killed
every soak scenario as a "hang" before it finished - raising only the outer job
timeout cannot fix this. Make the per-test timeout scale with the soak budget
(budget + headroom) when WINUI_LIFETIME_STRESS_MINUTES > 0, and keep the
original 5-minute default for all other test passes.
Broader coverage:
Lifetime bugs live across WinUI, not just ItemsRepeater. Add stress scenarios
for other high-risk subsystems, all using supported high-level APIs and
self-contained hosting:
* StressListViewContainerRecycling - ListView/GridView virtualization
(container generation/recycling via ModernCollectionBasePanel).
* StressPopupOpenClose - Popup open/close (popup root/overlay + hosted peer).
* StressNavigationViewMenuChurn - NavigationView menu-item/pane/selection churn.
* StressTabViewAddRemove - TabView tab container + content add/remove.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The suite could still fail the PR pipeline: only a detected leak was
non-gating (Log.Warning). A native crash / fail-fast (e.g. the
ItemsRepeater stowed exception in combase.dll), an unhandled managed
exception, or a hang would terminate the TAEF host and fail the gate,
because managed code cannot catch a native fail-fast and downgrade it.
Make the workload report-only by running it in only two situations:
* the scheduled soak pipeline (WinUI-LifetimeStress.yml), the one
place that sets WINUI_LIFETIME_STRESS_MINUTES > 0; and
* an explicit local opt-in via WINUI_LIFETIME_STRESS_ITERATIONS > 0.
In the per-PR gate and Nightly (neither variable set) every scenario
skips at the RunStress choke point, so the suite is still built,
discovered and reported but does no create/teardown/GC work and cannot
crash, throw or hang the gate. Classification=Integration is retained so
the Helix generator still emits the dedicated LifetimeStressTestSuite
work item the soak pipeline consumes. Docs/comments updated to match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The suite no longer runs as a gate on every test pass. Reflect that the workload runs only in the scheduled soak (WINUI_LIFETIME_STRESS_MINUTES) or an explicit local run (WINUI_LIFETIME_STRESS_ITERATIONS), skips in the PR gate and Nightly, and that ItemsRepeater is currently quarantined. Also add the new scenarios (ListView, Popup, NavigationView, TabView). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the suite skipped entirely outside the scheduled soak, so no
lifetime report was produced in PR runs. Make it run a small report pass
on every test pass (PR gate + Nightly) while guaranteeing it can never
fail the pipeline for anything a managed catch can reach:
* RunStress now runs DefaultReportIterations cycles by default (no env
var needed); WINUI_LIFETIME_STRESS_MINUTES still selects soak and
WINUI_LIFETIME_STRESS_ITERATIONS an explicit heavier run.
* New SafeUI wrapper catches exceptions INSIDE the UI-thread callback,
before RunOnUIThread.Execute can convert them into a Verify.Fail
(which records a Failed verdict a test-thread catch cannot undo). All
16 scenario UI calls now route through SafeUI.
* RunIterationReporting catches test-thread exceptions; leaks remain
Log.Warning via VerifyCollected(failOnLeak:false). Net: the suite
never records a Failed result, so Publish Test Results never fails.
The only uncatchable case is a genuine native crash / fail-fast that
kills the TAEF host - the real lifetime signal we want - and the one
known deterministic crasher stays quarantined ([TestProperty Ignore]).
Docs updated to match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The lifetime stress suite intentionally provokes native peer-lifetime
faults, so a run can end in a fail-fast that crashes the TAEF host
(te.exe) mid-suite. Two vectors let that break the Run Tests stage:
1. On a native crash te.wtl is never flushed, so no testResults.xml is
produced -> RunTestPassSliceOnBuildAgent.ps1 throws ("Expected
testResults.xml ...") and the whole slice fails / the suite vanishes.
2. A scenario that reports Fail lands as result="Fail" in
testResults.xml -> PublishTestResults@2 (failTaskOnFailedTests:true)
fails the stage.
Handle both in RunHelixWorkItem.ps1, scoped strictly to the lifetime
work item (detected via the LifetimeStressTestSuite TaefQuery) so no
other suite is affected:
* Skip the failed-test reruns (pointless for a stress suite and they
only add time and extra crash dumps).
* Tolerate a missing te.wtl instead of erroring.
* Guarantee a valid, zero-failure testResults.xml via a new
Set-LifetimeResultsNonGating helper: downgrade any Fail results to a
non-gating Skip (keeping the per-scenario detail), or emit a single
synthetic passing "report" entry when te.exe crashed and produced no
results file.
The full per-scenario report stays readable in the work item's console
log and in te_original.wtl; the suite is always present in the run and
never fails the pipeline.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 589f8ff8-5e78-4bcc-8075-4a4b817d2144
When an upstream CreateTestPayload leg is canceled/hung and never publishes its payload artifact, the RunTests job still ran (condition: not(failed())) and failed later with a cryptic 'Get-ChildItem: Cannot find path ...helixworkitems... PathNotFound' in RunTestPassSliceOnBuildAgent.ps1. - Require succeeded() on the RunTestPass job so it skips instead of running without a payload. - Fail fast with a clear, actionable message if the work item proj dir is missing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends the lifetime stress suite with 9 component-targeted scenarios that drive create/use/teardown/GC for areas that recur in Watson 'Lifetime Issues' buckets: MenuFlyoutPresenter, ResourceDictionary, ItemsSourceView, automation peers (CUIAWindow/AppBar/DependencyObjectPropertyAccess), Frame NavigationCache, XamlReader/XBF, SimpleProperty, MediaTransportControls, and InkToolbar. All remain report-only (non-gating), consistent with the suite design. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Protik Biswas (protikbiswas100)
requested a review
from Mike Crider (codendone)
September 11, 2026 13:26
…fields The existing *Native gating scenarios exercised NavigationView/Slider/ComboBox/ ScrollView/TextBlock, none of which touch the cross-boundary peer fields that were converted from raw ctl::ComPtr to TrackerPtr (ListViewBase::m_spContainerBeingClicked, ModernCollectionBasePanel layout-strategy/data-info-provider, the SplitView light- dismiss layer, ToggleSwitch knob/curtain transforms). The controls that do own those fields were only covered by non-gating report-only scenarios, so the suite could not actually surface (or validate a fix for) that native lifetime crash class. Add four gating native scenarios routed through RunNativeStress that specifically churn those fields and then drive the FINAL native release off the UI thread (GC + WaitForPendingFinalizers) - the cross-boundary/off-thread release path where a raw-ComPtr field faults and a TrackerPtr field does not: - StressListViewClickContainerChurnNative (click container + virtualizing panel) - StressGridViewContainerChurnNative (ListViewBase + backing panel) - StressSplitViewLightDismissChurnNative (dismiss-layer popup + dismiss elements) - StressToggleSwitchTransformChurnNative (knob/curtain transform peers) Aggressive counts run only when AggressiveNativeReproEnabled (soak / opt-in gate); the light per-PR pass does a small benign churn and cannot crash the shared pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
…ggressive repro In the $isLifetimeStress path of RunHelixWorkItem.ps1, detect a native TAEF-host crash (te.exe non-zero exit / new .dmp / a scenario that started but never completed), attribute it to the in-flight scenario from the "[LifetimeStress] NATIVE: scenario 'X' starting/completed" markers, and emit a non-gating ##vso[task.logissue type=warning]Native lifetime crash in scenario 'X' (dump: <name>) before the results are laundered green. This gives attribution without opening the dump and keeps the stage green. Name each new dump LifetimeStress-<scenario>-*.dmp and, in RunTestPassSliceOnBuildAgent.ps1, raise the per-slice dump upload cap (3 -> 10) and prioritize the attributed lifetime dumps so a native crash dump is never crowded out by unrelated dumps in the same slice. Also set WINUI_LIFETIME_STRESS_NATIVE=1 for the lifetime work item (unless a pipeline already provided a value) so the aggressive native-repro variant actually runs in te.exe's process env; pipeline-root variables do not propagate into the test host on their own. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b81a104-f576-4af1-972e-c3e04538186d
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
….processhost inherits it The aggressive native-repro flag was set only at Process scope on the launcher shell, which never reached the managed harness: the unpackaged test runs in te.processhost.exe, spawned by TAEF via a broker whose environment block is seeded from the User/Machine registry environment at creation rather than inherited from this shell (build 157605025 proved launcher=1 but harness read aggressiveNativeRepro=False for all 11 native scenarios). Write the value into the User (and best-effort Machine) registry environment before launching te so the broker-spawned te.processhost picks it up at creation; keep the Process-scope set for the direct-child/local-dev case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b81a104-f576-4af1-972e-c3e04538186d
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
The *Native scenarios previously pre-quiesced the converted cross-boundary peer
fields (nulled Pane/Content/ItemsSource/Child, closed panes, unsubscribed
handlers) BEFORE dropping the reference, so the off-thread finalize released an
already-clean peer - a managed churn + leak check that could not reproduce a
native use-after-free (e.g. StressSplitViewLightDismissChurnNative).
Each scenario now creates a genuine, control-specific dangerous condition:
* Stop pre-quiescing - keep the converted field populated up to unparent so
the final native release must tear down a live field off the UI thread.
* Reentrant teardown - mutate the converted field from inside a lifecycle
event (Unloaded/Toggled/SizeChanged/Loaded) while the native peer is
mid-unlink, guarded against loops.
* After-teardown access - touch a just-unlinked peer/container/leaf.
* Off-thread final release - via shared FinalizeOffThread().
Balanced (compiles-clean bracket check); behavior stays non-gating via SafeUI /
RunNativeStress (managed throws downgraded to warnings), aggressive variant
scales intensity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9b81a104-f576-4af1-972e-c3e04538186d
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
Protik Biswas (protikbiswas100)
added a commit
that referenced
this pull request
Sep 15, 2026
…racker-fixes-native-repro Port native lifetime-stress repro + crash-surfacing (from PR #11830), all leak checks gating
Each lifetime work item now writes a per-work-item LifetimeNativeCrashReport.json (native host crashes + non-gating native scenario warnings). A new PostTestRun step runs Report-LifetimeNativeCrashTotals.ps1 after the test run to total those records across the shard, print a single count, emit a non-gating warning, publish the LifetimeNativeCrashTotal variable, and write LifetimeNativeCrashSummary.json. Fail-open and non-gating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b81a104-f576-4af1-972e-c3e04538186d
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
Extend the lifetime-stress managed leak coverage to the container-realizing and popup controls in CreateControlSet() that had no dedicated churn scenario (RadioButtons, MenuBar, DropDownButton, SplitButton, InfoBar, TeachingTip). Adds a shared RunChurnScenario<TControl> harness that mirrors the existing hand-written churners' leak-probe shape (build a container, churn it while parented to fill the selection model / realized-container / popup cache, track a child object with a WeakReference, verify it collects). All new scenarios inherit the non-gating RunStress/SafeUI/VerifyCollected plumbing, so a residual reference is reported as a warning and never gates the pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b81a104-f576-4af1-972e-c3e04538186d
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
This was referenced Sep 16, 2026
…nfigs The PostTestRun aggregation ran in every test-pass job (one per testOS x buildFlavor) and wrote a workItemCount=0 LifetimeNativeCrashSummary.json in every job that ran no lifetime work item. The LifetimeStressTestSuite runs ONLY in the checked (chk) flavor (lifetime/TrackerHandle leak detection needs the reference-tracker instrumentation that free builds lack), so the fre-flavor jobs produced a scatter of empty summaries that look like the aggregation is broken (e.g. searchRoot 'c:\uploadroot\Win11-23H2\x64fre', workItemCount 0). Report-LifetimeNativeCrashTotals.ps1 now returns early when no per-work-item LifetimeNativeCrashReport.json is present: it still totals zero and sets the LifetimeNativeCrashTotal variable, but does not write an empty summary. The only LifetimeNativeCrashSummary.json in the artifacts is now the populated one from the chk job that actually ran the suite. Doc updated to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b81a104-f576-4af1-972e-c3e04538186d
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
Condense comments to at most 1-2 lines across the lifetime stress harness, Helix scripts, and pipeline YAML. Comment-only changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
Author
|
/azp run |
|
Azure Pipelines: 1 pipeline(s) were filtered out due to trigger conditions. |
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.
Summary
Introduces dedicated object-lifetime test coverage - Lifetime crashes (use-after-free, premature native peer destruction, ref-counting mistakes in the 3-layer peer model) are recurring and are never fixed in just one place so we need a standing harness that keeps catching them.
The suite is report-only: it never gates a PR. It surfaces lifetime bugs as a report from a scheduled soak pipeline instead of blocking the shared Run Tests stage (see How it plugs into CI for why a native crash cannot be downgraded to a warning).
Fixes
https://task.ms/64008072
Files changed
controls/test/MUXControlsTestApp/LifetimeStressTests.csbuild/WinUI-LifetimeStress.ymldocs/testing/lifetime-stress-tests.mdHelix/common/test/RunHelixWorkItem.ps1/testtimeoutwith the soak budget whenWINUI_LIFETIME_STRESS_MINUTES > 0(otherwise unchanged at 5 min).What's added
controls/test/MUXControlsTestApp/LifetimeStressTests.cs— a lifetime stress harness modeled on the existingLeakTests.cs. It repeatedly creates → parents → lays out → unparents → drops → collects across:[TestProperty("Ignore", "True")]) because it reproduces a deterministic native crash (0xC000027Bstowed exception incombase.dll); re-enable once that underlying ItemsRepeater lifetime bug is understood/fixed.How it "does better" than the old for-hours soak: every iteration aggressively drains the UI thread and forces
GC.Collect+WaitForPendingFinalizers+GC.Collect, so a dangling native peer faults promptly (on the iteration that created it) instead of eventually.build/WinUI-LifetimeStress.yml— a scheduled pipeline that builds WinUI and runs the suite in soak mode by settingWINUI_LIFETIME_STRESS_MINUTES.docs/testing/lifetime-stress-tests.md— rationale, CI behavior, and usage.How it plugs into CI
The class is tagged
[TestProperty("TestSuite", "LifetimeStressTestSuite")]and[TestProperty("Classification", "Integration")], so the existing Helix work-item generator (Helix/common/pipeline/GenerateHelixWorkItems.ps1) emits an isolated work item for the suite on every DevTestSuite pass — a lifetime crash is contained to its own work item instead of cascading into unrelated tests.Report-only — the suite never fails a PR. A real object-lifetime bug faults as a native crash / fail-fast (e.g. a stowed exception in
combase.dll) that terminates the TAEF test host. Managed code cannot catch a native fail-fast and downgrade it to a warning, so running the workload in the PR gate could fail the Run Tests stage and block unrelated PRs. To prevent that, the scenarios do their actual create/teardown/GC work only when explicitly asked:build/WinUI-LifetimeStress.ymlsetsWINUI_LIFETIME_STRESS_MINUTES > 0; each scenario loops on a wall-clock budget. This is the normal place the suite exercises anything.WINUI_LIFETIME_STRESS_ITERATIONS > 0for a fixed number of cycles.In the PR gate and Nightly — where neither variable is set — every scenario skips (logs a report line and returns). The suite is still built, discovered, and reported, but does no work and therefore cannot crash, throw, or hang the gate. Within a run that does execute, a residual (uncollected) reference is a soft signal: logged as a
Log.Warning, not a failed result.Configuration
Read from the environment. When neither is set (the default, including the PR gate and Nightly), scenarios skip — so the suite is safe everywhere by default.
WINUI_LIFETIME_STRESS_MINUTES0(disabled)WINUI_LIFETIME_STRESS_ITERATIONS0(skip)