Docs Style Lint
AvaloniaUI/avalonia-docs
Reviews and lints Avalonia documentation pages against house style rules, content boundaries, anti-marketing standards, accessibility, and SEO.
A skill your agent uses when optimizing AtomUI controls, investigating control performance regressions, changing Avalonia styled-property bindings, lazy creation, templates, selectors, or Gallery…
$ npx skills add AtomUI/AtomUI --skill atomui-control-performance -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install AtomUI/AtomUI atomui-control-performance --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/atomui-control-performance .claude/skills/atomui-control-performance && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "atomui-control-performance" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/atomui-control-performance into .claude/skills/atomui-control-performance/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "atomui-control-performance", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/atomui-control-performanceType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add AtomUI/AtomUI --skill atomui-control-performance -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install AtomUI/AtomUI atomui-control-performance --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/atomui-control-performance .agents/skills/atomui-control-performance && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "atomui-control-performance" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/atomui-control-performance into .agents/skills/atomui-control-performance/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "atomui-control-performance", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add AtomUI/AtomUI --skill atomui-control-performance -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install AtomUI/AtomUI atomui-control-performance --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/atomui-control-performance .cursor/skills/atomui-control-performance && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "atomui-control-performance" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/atomui-control-performance into .cursor/skills/atomui-control-performance/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "atomui-control-performance", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/AtomUI/AtomUI.git --path .agents/skills/atomui-control-performance--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add AtomUI/AtomUI --skill atomui-control-performance -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install AtomUI/AtomUI atomui-control-performance --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/atomui-control-performance .gemini/skills/atomui-control-performance && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "atomui-control-performance" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/atomui-control-performance into .gemini/skills/atomui-control-performance/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "atomui-control-performance", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install AtomUI/AtomUI atomui-control-performanceInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add AtomUI/AtomUI --skill atomui-control-performance -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/atomui-control-performance .github/skills/atomui-control-performance && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "atomui-control-performance" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/atomui-control-performance into .github/skills/atomui-control-performance/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "atomui-control-performance", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add AtomUI/AtomUI --skill atomui-control-performance -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install AtomUI/AtomUI atomui-control-performance --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/atomui-control-performance .opencode/skills/atomui-control-performance && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "atomui-control-performance" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/atomui-control-performance into .opencode/skills/atomui-control-performance/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "atomui-control-performance", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
atomui-control-performanceA skill your agent uses when optimizing AtomUI controls, investigating control performance regressions, changing Avalonia styled-property bindings, lazy creation, templates, selectors, or Gallery…
Atomui Control Performance is an agent skill from AtomUI/AtomUI. Use when optimizing AtomUI controls, investigating control performance regressions, changing Avalonia styled-property bindings, lazy creation, templates, selectors, or Gallery performance scenarios. Applies to controls such as Space, Button, Icon, AddOnDecoratedBox, LineEdit, and shared primitives.
Its SKILL.md is about 17k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Development. It works with Avalonia. The repository describes itself as: An enhancement and extension library for Avalonia, bringing the Ant Design design language, modern controls, theming, native integrations, and cross-platform UI capabilities to… The licence is LGPL-3.0.
5 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit d234fe0. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
rgdotnetgitFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use git, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Atomui Control Performance loads about 17k tokens when it runs. Until then it costs about 82 tokens; SKILL.md has 6,807 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from AtomUI/AtomUI at commit d234fe0, republished under its LGPL-3.0 licence (© AtomUI). 6,807 words, ~17,009 tokens.
.claude/skills/atomui-control-performance/SKILL.md (or your agent's skills folder).Correctness outranks performance. Every rule below exists to defend that priority. If any rule conflicts with correctness, correctness wins.
Violating any of these blocks merge. No exception, no tradeoff, no "it's only one case".
Correctness regression is unacceptable. Performance optimization must preserve control functionality, UI appearance, animation behavior, interaction semantics, theme behavior, and public API. Any visible or behavioral change caused by an optimization is a regression, not a tradeoff. If the user explicitly approves a behavior change, that becomes a separate non-perf change, not a perf optimization.
No logic bug introduction. This includes invalid visual states, broken state transitions, incorrect property precedence, stale layout/render state, lost interactions, incorrect lifecycle cleanup, race conditions in event chains, or Gallery-visible behavior mismatches. Discovering one mid-optimization means stop, fix or revert immediately, do not continue stacking perf changes on top.
No resource leak. Anything created/subscribed/bound/cached/lazily materialized/reparented must have a defined and verified release path before the change is considered complete. Leak scanning is mandatory during analysis, not optional cleanup afterward.
No measurable speed regression. If any primary metric for the targeted control or its real Gallery ShowCase gets worse under the same measurement policy, the change must be fixed, split, reverted, or explicitly reported as a blocker.
No materially more complex implementation logic. Fragile state machines, duplicated template/style logic in code, broad lifecycle orchestration, unclear synchronization, or code harder to reason about than the original behavior warrants — all indicate the optimization is too expensive in complexity. Stop and propose a simpler option instead. See Process Gate 3 for the numeric thresholds that flag this.
No theme element → C# dynamic creation. See Theme Static Rule. This is the highest-leverage rule in this document; nearly all rollback incidents stem from violating it.
No same-priority binding collisions for the same property. If a property has both an internal default binding and an external override binding, they must be at different BindingPriority levels. See Avalonia Binding Priority Guardrails.
No BindUtils.RelayBind + disposable plumbing for lifetime-consistent children. When source and target have the same owner and lifetime, use Avalonia [!] binding syntax. Disposable infrastructure is reserved for mismatched lifetimes, replacement paths, detach/re-template, global/window subscriptions, timers, or lazily materialized objects.
3-rounds optimization budget. For the same scoped target, three implementation-and-measurement rounds without primary speed metric improvement = stop. At end-of-optimization, if no worthwhile gain: restore every perf-only change, delete intermediate scratch code/docs, keep only correctness fixes / leak fixes / useful measurement tooling. Report exactly what was reverted, what correctness fixes were kept, what cleanup was done.
5-control pattern rollout circuit breaker. Same refactor pattern (e.g., dynamic creation, lazy popup materialization, binding restructure) applied to ≥ 5 controls = stop and audit. Two questions: (a) did at least 3 of 5 show measurable speed gain? (b) did any one introduce ≥ 2 Gallery-visible bugs? Either "no" / "yes" stops further rollout and triggers reflection before the 6th control.
No unsupported claims about framework behavior. Any assertion such as "X triggers Y", "Z is O(N)", "this binding takes the fast path", or "IsVisible=False is free" MUST be backed by the currently resolved dependency source or by a measurement with explicit methodology. Source inspection is evidence about the current implementation, not an AtomUI architecture contract. Long-term documents keep AtomUI invariants and revalidation conditions; exact external versions, paths and line numbers belong only in dated upgrade or investigation records. See Framework Behavior Evidence.
No _ignoreXxx flags in property-changed handlers. Adding a private bool (_ignoreSelectedPropertyChanged, IgnorePropertyChange, _isUpdating, _suppressNotification, etc.) to short-circuit property-changed dispatch hides bugs rather than fixing them. The Cascader / AbstractSelect rollbacks all follow this pattern. See Re-entrancy & Ignore-Flag Guardrails. The few legitimate cases are listed there; the default answer is "fix the event flow, do not add the flag".
Optimization qualification documented. Every perf commit must state — in the commit description — the realistic instance count for the targeted control in its busiest Gallery ShowCase, and the per-session frequency of the operation being optimized. If instance count ≤ 5 AND operation frequency < 1/session, the commit must explain why optimization is still warranted, or be rejected before measurement begins. The investigation playbook's qualification gate (see First-60-Minutes Investigation Playbook) is the operational form of this rule.
Benefit report is mandatory at completion. Every optimization turn MUST proactively report the benefit to the user before stopping, even if the user did not ask. Start with a one-sentence plain-language takeaway that says what became cheaper and where the user benefits. Then include a table with metric / baseline / optimized / formula / improvement / conclusion. Metric names must be user-readable and include units/scopes such as per close, per row, per DataGrid, or per Gallery page; do not report only internal method/class names or raw callsite counts. If the optimization is structural-only, report the structural count reduction (for example handlers per instance, bindings per item, visuals per root, objects allocated per operation) and its percentage, and explicitly say it does not claim page-load speedup unless timing proves it. If timing data is single-run smoke, label it smoke-only and do not present it as proof of speedup. If no valid before/after timing exists, explicitly say that no timing percentage is claimed and use the structural/correctness result as the benefit. This is part of the definition of "done".
Use the source matching the currently resolved Avalonia dependency to verify framework-behavior hypotheses. Every framework-behavior claim in a review or commit description must cite current implementation evidence or reproducible measurement. Revalidate source-derived claims after dependency upgrades, and never promote a particular external version, commit, local path, or line number into AtomUI's long-term architecture contract.
The verified cost model and counter-intuitive points are maintained in this skill. Use the Cost Model below for both the evidence summary and the operational rule.
Avalonia.Base/AvaloniaProperty.cs, AvaloniaObject.cs, PropertyStore/ValueStore.cs, Data/BindingPriority.cs, PropertyStore/FramePriority.csAvalonia.Base/Data/TemplateBinding.cs, Data/TemplateBindingExpression.cs, Data/Core/BindingExpression.cs, Data/Core/MultiBindingExpression.cs, AvaloniaObjectExtensions.cs, PropertyStore/DirectBindingObserver.cs, Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.csAvalonia.Base/Layout/LayoutManager.cs, Layout/Layoutable.cs, Layout/LayoutQueue.cs, Visual.csAvalonia.Base/Visual.cs (AffectsRender, InvalidateVisual), Rendering/IRenderer.cs, Rendering/ImmediateRenderer.cs, Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs, Media/SolidColorBrush.cs, Media/Pen.cs, Media/StreamGeometry.csAvalonia.Base/Interactivity/RoutedEvent.cs, Interactivity/Interactive.cs, Interactivity/EventRoute.cs, Input/InputElement.csAvalonia.Base/Styling/Selector.cs, Styling/TemplateSelector.cs, Styling/Activators/PropertyEqualsActivator.cs, Styling/Activators/StyleClassActivator.cs, Styling/Activators/NthChildActivator.cs, Styling/ControlTheme.cs, Styling/Setter.cs, Styling/PropertySetterTemplateInstance.cs, Styling/Styles.cs, StyledElement.csAvalonia.Controls/Primitives/TemplatedControl.cs, Controls/Presenters/ItemsPresenter.cs, Controls/Presenters/ContentPresenter.cs, Controls/Templates/DataTemplateExtensions.cs, Controls/ItemsControl.cs, Controls/VirtualizingPanel.csAvalonia.Controls/Primitives/Popup.cs, Primitives/PopupRoot.cs, Primitives/OverlayPopupHost.csAvalonia.Base/Threading/DispatcherPriority.cs, Threading/Dispatcher.cs, Threading/Dispatcher.Invoke.cs, Threading/Dispatcher.Timers.csAvalonia.Base/Animation/Animatable.cs, Animation/Transition.csItems still pending source verification are marked [VERIFY]. Decisions must not depend on [VERIFY] items. The current open list lives in §13 of the pitfalls doc.
Functional visuals defined in
ControlTemplate/ControlThemestay in axaml. Moving them to C#EnsureXxx()/ClearXxx()dynamic creation as a performance optimization is prohibited by default.
The cost asymmetry is overwhelming:
| Item | axaml static + IsVisible toggle | C# dynamic Ensure/Clear |
|---|---|---|
| Instantiation | Template inflation once | Each Ensure rebuilds |
| Hidden cost | IsVisible=False short-circuits MeasureCore, ArrangeCore, and ImmediateRenderer.Render (Avalonia.Base/Layout/Layoutable.cs:546, :671; Avalonia.Base/Rendering/ImmediateRenderer.cs:34) | Active until ClearXxx releases |
| TemplateBinding | Reactive via XAML, direct PropertyChanged subscription (Avalonia.Base/Data/TemplateBindingExpression.cs:37-43) | Manual SetCurrentValue + property change handlers |
| Style/Selector | Cascade automatically — but only if child has TemplatedParent, see below | Each rule re-implemented in C# |
| Lifecycle | Avalonia auto-managed | Manual disposables + ordered teardown |
| Race / ordering bugs | ~zero | High frequency (the rollback evidence) |
The "saved" cost of a hidden axaml node is one Visual instance plus its initial template inflation. Every rollback in §[Incident Callouts] confirmed that this saving was outweighed by the ongoing churn of manual Ensure*/Clear* plus the lost /template/ cascade. The complexity cost of dynamic creation is permanent and compounds across controls.
/template/ selector requires TemplatedParent — non-negotiableIf you must create template children in C# (one of the three exceptions below), the framework will not match /template/ selectors against them unless TemplatedParent is set. TemplateSelector.Evaluate returns NeverThisInstance when control.TemplatedParent == null (Avalonia.Base/Styling/TemplateSelector.cs:39-49). That means every theme rule like ^[X=true] /template/ Foo#bar { ... } silently stops applying to the new C# child.
Required handling whenever a template child is created in C#:
child.SetTemplatedParent(this) immediately after creation.child.SetTemplatedParent(null) in the symmetric Clear* path or OnDetachedFromVisualTree.ItemsControl / ScrollViewer / TreeView / CascaderView / calendar/time panels / large layout trees. The lightweight Popup shell still stays in axaml; only the popup's CONTENT may be deferred. See Popup Lazy Content Rule.ControlTemplate (architectural constraint, see Avalonia.Controls/Primitives/AdornerLayer.cs). Notification cards, overlay dialogs, drawer overlays, tooltips fall here.ItemsControl + ItemTemplate; only when ItemsControl is structurally insufficient may Children.Add be used directly.Any "fourth case" requires opening a discussion with the user and updating this skill before merge.
IsVisible="False" + selector instead<!-- ✅ Correct: stay in axaml, hide by default, reveal via selector -->
<atom:CheckBox Name="ToggleCheckbox"
IsChecked="{TemplateBinding IsChecked, Mode=TwoWay}"
IsVisible="False" />
<Style Selector="^[ToggleType=CheckBox] /template/ atom|CheckBox#ToggleCheckbox">
<Setter Property="IsVisible" Value="True" />
</Style>// ❌ Wrong: do not move axaml elements into C# dynamic creation
private CheckBox? _toggleCheckbox;
private CompositeDisposable? _toggleCheckboxDisposables;
private void EnsureToggleCheckbox() { /* ... */ }
private void ClearToggleCheckbox() { /* ... */ }Before approving any perf commit, grep new EnsureXxx() / ClearXxx() introductions and confirm each falls into one of the three allowed exceptions. If not, the commit fails this hard boundary.
This is the implementation of Theme Static Rule exception 1.
Popup shell stays in ControlTheme. It preserves placement, light-dismiss, overlay, theme styling, and required template contracts.TreeView, CascaderView, calendar/time panels, complex item presenters, filter lists, empty indicators, large popup layout trees.The following model records implementation evidence used to form and falsify performance hypotheses. Source coordinates are review aids for the currently resolved dependency and must be revalidated after upgrades; the AtomUI implication and its measurement gate are the durable parts. Use each entry's evidence, AtomUI implication, and measurement gate as the complete review record.
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
StyledProperty read | ValueStore.GetValue does a dict lookup on _effectiveValues, falls back to inherited/default; no per-frame priority resolution on read. | Avalonia.Base/PropertyStore/ValueStore.cs:286-294, Avalonia.Base/AvaloniaObject.cs:251-258 | Reads are O(1) but allocate-free only when the entry exists. |
DirectProperty read | Direct getter delegate; bypasses ValueStore entirely. | Avalonia.Base/DirectPropertyBase.cs | Cheapest read path. Use ONLY for runtime-state properties never controlled by Style / Animation / Template. |
BindingPriority order | Animation = -1, LocalValue = 0, StyleTrigger = 1, Template = 2, Style = 3, Inherited = 4, Unset = int.MaxValue. Lower numeric = higher priority (wins). | Avalonia.Base/Data/BindingPriority.cs:9-50, Avalonia.Base/PropertyStore/FramePriority.cs:6-35 | Internal token defaults vs user override must live on different priority frames. Same-priority writes Dispose each other. |
IsSet(property) | True for any EffectiveValue entry, regardless of source (Style / Trigger / Template / LocalValue / Animation). | Avalonia.Base/PropertyStore/ValueStore.cs:352, Avalonia.Base/AvaloniaObject.cs:298-304 | Cannot be used as "user explicitly set this" check. Past Space incident roots here. |
SetCurrentValue vs SetValue vs ClearValue | SetValue writes at LocalValue (overrides lower priorities); SetCurrentValue writes at the current effective frame without disturbing bindings; ClearValue removes the LocalValue entry to let lower-priority sources show through. | Avalonia.Base/AvaloniaObject.cs:333-355, :407-422 | Theme/template default initialization → SetCurrentValue. "Reset to default" → ClearValue, never SetValue(default). |
AffectsMeasure / AffectsArrange / AffectsRender | Static registration subscribes once to property.Changed; cost is paid only when the registered property actually changes. | Avalonia.Base/Layout/Layoutable.cs:502-512, Avalonia.Base/Visual.cs:446-500 | Don't flag every property "for safety". Hot-frequency properties carrying AffectsRender cost one InvalidateVisual per change. |
RaisePropertyChanged with no subscribers | Null-checks _propertyChanged; the bare property change is cheap. | Avalonia.Base/AvaloniaObject.cs:761-806 | Cost is dominated by Affects* + selector activators + binding observers, not the raise itself. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
TemplateBinding | Allocates a single TemplateBindingExpression and subscribes directly to templatedParent.PropertyChanged. No expression-node walk. | Avalonia.Base/Data/TemplateBinding.cs:57-68, Data/TemplateBindingExpression.cs:37-43 | All same-owner same-lifetime template bindings must be {TemplateBinding X}, never {Binding RelativeSource={RelativeSource TemplatedParent}, Path=X}. |
[!] direct binding | AvaloniaObjectExtensions.Bind(IObservable<T>) → DirectBindingObserver<T>; one observer object, one disposable. | Avalonia.Base/AvaloniaObjectExtensions.cs:188-204, PropertyStore/DirectBindingObserver.cs:7-84 | Lifetime-matched parent ↔ template-child is [!]. Tier 1 §8 binds this. |
BindUtils.RelayBind | AtomUI wrapper that constructs a full Binding + Path and calls target.Bind(...). Strictly heavier than [!]. | AtomUI source under src/AtomUI.Base/ | Only for mismatched lifetimes, replacement paths, detach/re-template, conditional bindings. |
MultiBinding + Converter | Each child observable change fires OnChanged; once all children initialized, Converter.Convert runs once per change. | Avalonia.Base/Data/Core/MultiBindingExpression.cs:23-49, :86-98, :116 | Converter must be stateless and allocation-free. High-frequency child binding pulls converter with it. |
$parent[T] / LogicalAncestorElementNode | Subscribes to Attached/DetachedFromLogicalTree; recomputes GetLogicalAncestors().ElementAtOrDefault(level) on every tree change, not cached. | Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs:59-68, LogicalTree/ControlLocator.cs:63-68 | Avoid in templates that re-attach often. Push state to a control-level StyledProperty and TemplateBinding from the popup content instead. |
Compiled bindings (x:DataType) | XAML compiler emits direct IL property access nodes; no reflection at runtime. | Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs:140-200, Avalonia.Base/Data/CompiledBinding.cs:195-235 | Hot-path Gallery / VM bindings should be compiled. ControlTheme bindings are already covered by TemplateBinding. |
Forgotten Bind(...) IDisposable | BindingExpression._source is a WeakReference<object?>; target holds the binding. No source-leak, but the subscription stays live. | Avalonia.Base/Data/Core/BindingExpression.cs:26-90, Data/Core/UntypedBindingExpressionBase.cs:100-113 | Bindings created in detach/re-attach paths must go in CompositeDisposable. axaml [!] is auto-managed by templated child lifetime. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
IsVisible=False short-circuit | MeasureCore returns immediately outside if (IsVisible); same for ArrangeCore; ImmediateRenderer returns at the top when visual is not visible. | Avalonia.Base/Layout/Layoutable.cs:546, :671; Avalonia.Base/Rendering/ImmediateRenderer.cs:34 | Confirmed free. Backbone of Theme Static Rule. |
IsVisible setter | Triggers UpdateIsEffectivelyVisible recursively + parent ChildDesiredSizeChanged + own InvalidateMeasure; resets DesiredSize to default. | Avalonia.Base/Layout/Layoutable.cs:842-868, Avalonia.Base/Visual.cs:507-509 | Don't flip IsVisible per frame. Don't use it as a "force re-measure" hack. |
InvalidateMeasure | Sets local IsMeasureValid=false, enqueues with LayoutManager. Does NOT walk to root unless child DesiredSize actually changes. | Avalonia.Base/Layout/Layoutable.cs:443-459, :480-486; Avalonia.Base/Layout/LayoutManager.cs:304 | Cheap by itself. Cost is the subsequent MeasureOverride. |
LayoutManager queue | Per-pass deduplication via LayoutQueue._loopQueueInfo; max 10 passes per tick; scheduled via MediaContext.BeginInvokeOnRender. | Avalonia.Base/Layout/LayoutManager.cs:23, :116-179, :348-355; Layout/LayoutQueue.cs:48-68 | N attribute changes in one frame ⇒ one MeasureOverride. Don't bother batching property setters for that reason alone. |
Grid star-sizing | 4 measurement groups + cyclic-dependency loop up to c_layoutLoopMaxCount. Not "always 2 passes". | Avalonia.Controls/Grid.cs:234-527 | Avoid star-grid as default panel. AtomUI conventions already favor DockPanel / StackPanel / FlexPanel. |
| Reparenting | Triggers OnDetachedFromVisualTreeCore → OnAttachedToVisualTreeCore → LayoutHelper.InvalidateSelfAndChildrenMeasure (whole subtree) + OnTemplatedParentControlThemeChanged. | Avalonia.Base/Visual.cs:715-738, :551, :776-791; Layout/Layoutable.cs:872-877 | Almost never worth doing for perf. Use container recycling, not manual reparent. |
EffectiveViewportChanged | First subscriber registers with LayoutManager._effectiveViewportChangedListeners; raised every layout pass for every listener. | Avalonia.Base/Layout/Layoutable.cs:169-190; Layout/LayoutManager.cs:219-220, :357-396 | Don't subscribe per item. One subscriber per scrolling host is fine. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
InvalidateVisual | Calls Renderer.AddDirty(this); queues for next render tick. Not synchronous. | Avalonia.Base/Visual.cs:418-421; Rendering/IRenderer.cs:30-33 | Multiple invalidations per frame coalesce. Don't double-invalidate via both AffectsRender and explicit calls. |
| Compositor-only animations | Opacity / Offset / Scale / RotationAngle / Translation / AnchorPoint / CenterPoint route to compositor without UI-thread layout. Size / ClipToBounds still pulls UI thread. | Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs:89-118 | Show/hide animations: Opacity + RenderTransform. Never animate Width/Height. |
SolidColorBrush / Pen | Mutable StyledProperty carriers with full change notification. ImmutableSolidColorBrush / ImmutablePen exist for cached use. | Avalonia.Base/Media/SolidColorBrush.cs:13-95; Media/Pen.cs:17 | Custom Render(DrawingContext) must cache brushes/pens, not new them per frame. |
StreamGeometry.Parse(string) | Re-parses the path string each call via PathMarkupParser. | Avalonia.Base/Media/StreamGeometry.cs:34-44 | Path data lives in axaml resources or generated metadata, not in repeated C# Parse calls. |
IsHitTestVisible=false vs IsVisible=false | IsHitTestVisible only skips hit-test. IsVisible=false skips measure / arrange / render. | Avalonia.Base/Input/InputElement.cs:64-65; Visual.cs:58-59; Rendering/ImmediateRenderer.cs:34 | Want "no layout/render cost" → IsVisible=false. Want "still visible but inert" → IsHitTestVisible=false or IsEnabled=false. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
| Route construction | Built eagerly per RaiseEvent, walks InteractiveParent chain into a pooled EventRoute. | Avalonia.Base/Interactivity/Interactive.cs:143-177; Interactivity/EventRoute.cs:13 | Cost is O(depth × handler-count). High-frequency events should be filtered at source before raise. |
Handled = true | Does NOT abort the route. Subsequent handlers see e.Handled == true; only those registered with handledEventsToo still run. | Avalonia.Base/Interactivity/EventRoute.cs:158-170; Interactivity/RoutedEventArgs.cs:43 | Class-handler cleanup that must always run requires handledEventsToo: true. |
| Class vs instance handler | Class handlers subscribe once to RoutedEvent.Raised; instance handlers go in per-control _eventHandlers dict. | Avalonia.Base/Interactivity/RoutedEvent.cs:84-94; Interactivity/Interactive.cs:16, :39-41, :181 | Default control-library OnXxx overrides should register via AddClassHandler<T>, not per-instance. |
PointerEntered / PointerExited routing | Both registered as RoutingStrategies.Direct. No tunnel/bubble. | Avalonia.Base/Input/InputElement.cs:144-147, :152-155 | Nested hover regions each receive their own enter/exit. Prefer :pointerover selector over hand-managed handlers. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
/template/ selector | Returns NeverThisInstance if control.TemplatedParent == null. C# Children.Add without SetTemplatedParent(this) ⇒ no match. | Avalonia.Base/Styling/TemplateSelector.cs:39-49 | If you go to C# child creation, you MUST set TemplatedParent and clear it on teardown. See Theme Static Rule. |
| Selector activator | Each selector subscribes to its dependency observable; ReevaluateIsActive runs on every dependency change. | Avalonia.Base/Styling/Selector.cs:44-68; Styling/Activators/PropertyEqualsActivator.cs:25-40 | Pseudo-class toggles are O(1). Compound selectors (^[X=true]:pointerover) re-evaluate on EVERY input — keep them shallow. |
ControlTheme.BasedOn | Linear recursive walk in ApplyControlTheme. No caching. | Avalonia.Base/StyledElement.cs:776-793 | BasedOn chain ≤ 3 levels. AtomUI internal token themes already meet this. |
Setter application | Plain Setter applies at StyleBase.Attach. PropertySetterTemplateInstance lazy-builds with _value ??= _template.Build(). | Avalonia.Base/Styling/Setter.cs:69-92; Styling/PropertySetterInstance.cs:45-71; Styling/PropertySetterTemplateInstance.cs:7-31 | Heavy template-valued setters are effectively lazy and OK to leave in. Plain setters are not. |
Runtime Styles.Add/Remove | Triggers full re-attach over the scope; no fast path. | Avalonia.Base/Styling/Styles.cs:286-310, :266, :282 | Don't mutate Application.Styles or scoped Styles in hot path. Theme variant switching goes through ThemeVariant. |
:nth-child / :nth-last-child | NthChildActivator re-evaluates on ChildIndexChanged; needs an IChildIndexProvider. | Avalonia.Base/Styling/Activators/NthChildActivator.cs:46-76 | Custom virtualizing panels must provide O(1) GetChildIndex for :nth-child to be free. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
ApplyTemplate | Runs during MeasureCore, after styling pass. Cached in _appliedTemplate; not rebuilt unless template actually changed. | Avalonia.Controls/Primitives/TemplatedControl.cs:306-348, :316, :123 | OnApplyTemplate is the cheap place to wire PART_ refs. Don't pretend it's a one-shot — re-templating happens. |
ItemsPresenter realization | if (Panel is VirtualizingPanel v) v.Attach(ItemsControl); else _generator = new ItemContainerGenerator(...); | Avalonia.Controls/Presenters/ItemsPresenter.cs:86-119, :107-110, :150-154; Controls/VirtualizingPanel.cs:133 | List-shaped controls default to VirtualizingStackPanel. Don't expose ItemsPanel as a runtime swap. |
DataTemplate lookup | Walks logical tree on every Content/ContentTemplate change; matches each candidate template. Not cached across instances. | Avalonia.Controls/Templates/DataTemplateExtensions.cs:20-62; Controls/Presenters/ContentPresenter.cs:633, :640-650 | Use IRecyclingDataTemplate for list scenarios. Avoid N mutually-exclusive DataTemplates. |
ItemsControl.Items change | Forwards NotifyCollectionChangedAction to panel; no diffing. Reset = full rebuild. | Avalonia.Controls/ItemsControl.cs:639-656 | Filter / refresh APIs should emit Add/Remove/Replace, not Reset. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
| Window-host vs overlay-host | Popup.Open calls OverlayPopupHost.CreatePopupHost; window host calls topLevel.PlatformImpl?.CreatePopup() (native window, expensive). Overlay host reuses PopupOverlayLayer (cheap). | Avalonia.Controls/Primitives/Popup.cs:449, :791-804; Primitives/OverlayPopupHost.cs:74, :158-160, :164-166 | Frequent open/close popups → overlay. AtomUI defaults are correct here. |
| Routed events through popup boundary | PopupRoot.InteractiveParent => (Interactive?)Parent, where Parent is the logical Popup. Events bubble through. | Avalonia.Controls/Primitives/PopupRoot.cs:96 | Routed events DO traverse the popup boundary via logical ancestry. Use that for state propagation. |
| Visual ancestry through popup boundary | PopupRoot is a separate visual root; GetVisualAncestors does NOT cross it. | Avalonia.Controls/Primitives/PopupRoot.cs:96 | Code that needs visual ancestor (TransformToVisual, adorner positioning) must explicitly account for the host boundary. |
| Light dismiss | Per-TopLevel LightDismissOverlayLayer, not global capture. | Avalonia.Controls/Primitives/Popup.cs:542, :557 | Nested popups must filter their own outside-click logic; the framework only gives you per-host dismiss. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
DispatcherPriority | int Value. Order: SystemIdle < ApplicationIdle < ContextIdle < Background < Input < Default(0) < Loaded < UiThreadRender < AfterRender < Render < BeforeRender < AsyncRenderTargetResize < DataBind < Normal < Send. Higher value = runs first. | Avalonia.Base/Threading/DispatcherPriority.cs:25-127 | "Wait until just after attach/load" → DispatcherPriority.Loaded. Default Post runs at Default, can be preempted by Render / Input / DataBind. |
Post / InvokeAsync / Invoke | Post and InvokeAsync are O(1) enqueue. Invoke is synchronous; on UI thread + Send it runs immediately, otherwise blocks. | Avalonia.Base/Threading/Dispatcher.Invoke.cs:107-111, :257-260, :616-620 | Post is NOT "in a moment". Don't use Post to fix timing — fix the event flow. |
DispatcherTimer | Each tick scans the timer list to compute next due time. | Avalonia.Base/Threading/Dispatcher.Timers.cs:52-65, :90 | Avoid one timer per item. Centralize short-lived timers. |
| Topic | Verified behavior | Source | Implication |
|---|---|---|---|
Animatable.Transitions activation | EnableTransitions() is called on OnAttachedToVisualTree. Until then, CollectionChanged is not subscribed and value changes don't run transitions. | Avalonia.Base/Animation/Animatable.cs:62-103, :87-103 | Setting Transitions in constructor is fine. Initial-value setup before attach won't animate; explicit DisableTransitions / EnableTransitions brackets are still required when you set values during initialization mid-tree. |
These are the rules that catch new contributors most often. Each was confirmed (or refined) against source.
IsVisible=False is genuinely free. Three-way short-circuit at Layoutable.cs:546, :671, ImmediateRenderer.cs:34. This is the foundation of Theme Static Rule.TemplateBinding is its own fast path, not a flavor of Binding. TemplateBindingExpression subscribes directly to templatedParent.PropertyChanged. {Binding RelativeSource=...} is materially heavier — never substitute it (Avalonia.Base/Data/TemplateBindingExpression.cs:37-43 vs Data/Core/BindingExpression.cs:60-134).DirectProperty reads bypass the value store, so they cannot be overridden via Style / Animation / LocalValue priorities (Avalonia.Base/DirectPropertyBase.cs). Use them only for runtime-state that's never theme- or style-driven./template/ selectors require TemplatedParent. A C# child added with Children.Add(...) and no SetTemplatedParent(this) silently fails to match ^... /template/ ... rules (Avalonia.Base/Styling/TemplateSelector.cs:39-49). This is the silent-cascade-loss footgun that most "C# dynamic" optimizations actually hit.$parent[T] is recomputed on every (de)attachment, not cached. It uses LogicalAncestorElementNode + ControlLocator.Track (Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs:59-68). For state that flows Popup ↔ content, store on the templated control and TemplateBinding from the popup content.[!] direct binding holds a strong ref source → target. Same-lifetime parent-child pairs are auto-managed (template lifetime). Cross-lifetime usage MUST go through BindUtils.RelayBind + CompositeDisposable (Avalonia.Base/PropertyStore/DirectBindingObserver.cs:7-84).Dispatcher.UIThread.Post(action) is enqueue, not "in a moment." Render, DataBind, Input, Loaded all preempt default priority (Avalonia.Base/Threading/DispatcherPriority.cs:25-127). If you find yourself reaching for Post to fix timing, the event flow is wrong.AffectsRender registration is lazy. Subscription is wired once at type registration; cost is only paid when the registered property actually changes (Avalonia.Base/Visual.cs:446-500). A property with no AffectsX registration is genuinely free of layout/render side-effects.^[Foo=true]:pointerover re-evaluate on Foo change AND on hover state change. Keep selectors shallow (Avalonia.Base/Styling/Selector.cs:44-68).ControlTheme.BasedOn is a linear recursive walk, no caching (Avalonia.Base/StyledElement.cs:776-793). Keep BasedOn chains ≤ 3 deep.PopupRoot.InteractiveParent returns the logical Popup (Avalonia.Controls/Primitives/PopupRoot.cs:96), so RoutedEvent bubbling works. But PopupRoot is a separate visual root, so GetVisualAncestors and TransformToVisual(window) don't cross it. Pick the right ancestry mode for the situation.IsSet(property) is "any effective value entry exists", not "user explicitly set this" (Avalonia.Base/PropertyStore/ValueStore.cs:352). The Space ItemSpacing/LineSpacing incident roots here.BindingPriority numeric ordering is verified at Avalonia.Base/Data/BindingPriority.cs:9-50.When a "X is slow" report comes in, walk this tree before writing any code. Each step has a cut-off — stop early instead of forcing a fix.
报告 / 假设:"X 慢"
│
▼
Step 1. 测量复现
│ - DevTools 触发 X,记录: layout pass 数 / property change 数 / allocation rate
│ - AtomUI.Performance 跑 cold + repeated samples (≥10 cold)
│ - 没有数字 → 拒绝优化(不许 fix vibes)
│
▼
Step 2. 识别 Avalonia 子系统
│ - 落在 Cost Model 哪个章节?
│ - 该子系统的单位成本量级是多少?(必须有 source 引用,不许猜)
│ - 实际触发频率是多少?(测量得来,不许估)
│ - 单位成本 × 频率 ≥ 1ms / 帧预算占比 ≥ 5% ?
│
├── 否 → 不是真瓶颈,没有优化空间 → 拒绝
│
└── 是 → Step 3
│
▼
Step 3. Avalonia 是否已提供更便宜的等价路径?
│ - axaml 静态 + IsVisible/Selector 能否实现?
│ - DirectProperty 是否合适(无须 styled value frame)?
│ - 编译绑定 (CompiledBinding) 能否替换反射绑定?
│ - Static cached Pen/Brush 能否替换每次创建?
│ - Method group dispatcher callback 能否替换 lambda?
│ - 已有的 utility (MathUtils 等) 能否替换手写代码?
│
├── 是 → 走该路径,无需自定义代码
│
└── 否 → Step 4
│
▼
Step 4. 自定义代码的复杂度成本(参见 Tier 1 §5 + Process Gate 3)
│ - 触发 Theme Static Rule?(默认禁止,例外 1/2/3 之外的拒绝)
│ - 新增 Ensure/Clear 链?
│ - 新增 disposable / 标志位?
│ - 复杂度增长是否超过 Gate 3 阈值?
│
├── 是 → 重新评估收益是否值得复杂度,不值得就放弃
│
└── 否 → Step 5
│
▼
Step 5. 实现 + 测量 + Gallery 矩阵
│ - 不通过 Tier 1 任一边界 = 回退
│ - 3 轮 (Tier 1 §9) 没改善 = 回退
│ - 5 控件批量 (Tier 1 §10) 触发 audit
│ - 通过 Process Gates → 准备 commitThis is the operational form of the Decision Tree. Walk it in order whenever a "X is slow" report comes in. Stop early at any step that disqualifies the optimization — most reports stop at Step 1 or Step 2.
Three yes/no questions. Any "no" closes the request without further investigation.
<Control>ShowCase in controlgallery/AtomUIGallery.Desktop and count.)If any answer is no, the answer is: "this control does not qualify for optimization — Tier 1 §13". Record the numbers, close the request.
dotnet run --project controlgallery/AtomUIGallery.Desktop/AtomUIGallery.Desktop.csprojControl-level baseline (low noise, single-control or small composition):
dotnet build tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -c Debug --framework net10.0
dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj \
-c Debug --framework net10.0 --no-build -- \
--suite <control-key> --count 60 \
--markdown /tmp/<control>-control-baseline.mdFind <control-key> in tools/performances/README.md ("当前命令" 节), or look under tools/performances/AtomUI.Performance/Suites/<Control>/ for an existing suite. If none exists, create the smallest representative suite first; building it is part of the work, not a precondition that blocks measurement.
State / lifecycle verification, when the control has it:
dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj \
-c Debug --framework net10.0 --no-build -- \
--verify-<control>-statesGallery-level baseline (real ShowCase, includes page-level fixed costs):
dotnet build tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj -c Debug --framework net10.0
dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj \
-c Debug --framework net10.0 --no-build -- \
--showcase <showcase-key> --label baseline \
--cold-iterations 10 --iterations 30 --warmup 5 --timeout-ms 30000 \
--markdown /tmp/<control>-showcase-baseline.mdSample policy: --cold-iterations 10 is the minimum for cold-first-navigation conclusions. Single-process samples are smoke-only and may NOT be reported as proof of improvement. Same --warmup/--iterations/--cold-iterations before and after, no exceptions.
Allocation / GC observation while interacting (optional, runs against the live Gallery process):
dotnet-counters monitor -p <gallery-pid> --counters System.RuntimeWatch gen-0/1/2-gc-count, alloc-rate, gc-heap-size. An allocation drop without a time drop is still a win, but only if you can name which allocations were removed.
Match findings to the Cost Model. Which subsystem dominates the measured time / allocation?
MeasureOverride runs, how many times per pass? Is IsVisible=False short-circuit eligible (Avalonia.Base/Layout/Layoutable.cs:546, :671)? Is a Grid doing 4-pass star resolution? Reparenting in a hot path?{Binding RelativeSource=...} instead of {TemplateBinding} (Avalonia.Base/Data/TemplateBindingExpression.cs:37-43)? Are there BindUtils.RelayBind calls where [!] would do? $parent[T] recomputation in a re-attach loop?^[X=true]:pointerover chained)? BasedOn chain ≥ 4 levels (Avalonia.Base/StyledElement.cs:776-793)?RaiseEvent whose route depth or handler count is large (Avalonia.Base/Interactivity/Interactive.cs:143-177)? Class handler vs per-instance handler usage?new SolidColorBrush(...) per render (Avalonia.Base/Media/SolidColorBrush.cs:13-95)? StreamGeometry.Parse(string) repeated (Avalonia.Base/Media/StreamGeometry.cs:34-44)? Width/Height animation pulling layout pass?Avalonia.Controls/Primitives/Popup.cs:449)? Heavy popup content created at default closed state (Popup Lazy Content Rule)?Use BindingDiagnostics.IsLoggingEnabled = true in dev build to surface binding failures masquerading as perf problems.
If none of the above lights up, the bottleneck may be Gallery-level page setup or another control on the same page. Re-run the Gallery baseline with the suspect control replaced by an empty placeholder; if the time stays the same, the original control is innocent.
cost × frequency ≥ 1 ms / frame budget OR ≥ 5 % of measured ShowCase time → optimization candidate, proceed to Decision Tree Step 3.docs/superpowers/progress/<date>-<control>-performance.md so the next investigation does not repeat the work.path:line reference.Without all four, you have not earned the right to write code. Tier 1 §11 (no unsupported claims) and §13 (qualification documented) both gate here.
Mandatory commit-time gates. A perf commit description without all of them filled in fails review.
[ ] 被优化的成本属于哪个框架子系统?
(Property / Binding / Layout / Render / Event / Popup / Dispatcher / Style / Animation)
[ ] 在该子系统的 Cost Model 中,当前实现证据或可复现测量:
___________________
[ ] 该子系统的单位成本量级(来自 Cost Model):______
[ ] 实际触发频率(来自测量,不许估):______
[ ] 总成本占比(≥ 5% 才进入下一步):______
[ ] 假设证伪点(如果实测低于此量级,优化应放弃):______如果某条引用还在 [VERIFY] 阶段,说明该子系统认知不足以支持优化决策。先在本 Skill 的 Cost Model 中补全实现证据、AtomUI 影响和测量门槛,再继续优化。
[ ] 行为/外观/动画/交互/主题/API 全部不变?
验证方式: ___________________
[ ] 通过 per-control regression matrix (Suites/<Control>/Regression.md)。
跑过的 case: ___________________
[ ] Gallery ShowCase 走查完成,覆盖控件: ___________________[ ] 列出本次新增的: subscription / binding / timer / lazy materialized object /
cached value / reparented element
[ ] 每一项的释放触发器已识别且验证。
[ ] 粘贴 "Resource Leak Detection" 章节 grep 命令的输出。[ ] 新增 Ensure*/Clear*/Sync* 方法 N 个 (阈值 ≥ 4 = 需要论证)
[ ] 新增 try/finally 标志位 M 个 (阈值 ≥ 2 = 需要论证)
[ ] 同一文件新增 disposable 字段 K 个 (阈值 ≥ 3 = 需要论证)
[ ] axaml 删除行数 vs C# 增加行数: ___ vs ___
[ ] Theme Static Rule 检查: 是否把 axaml 节点搬进 C#?Y/N
若 Y,对应三类例外的哪一类?___________________任一阈值超出必须显式论证(不接受"为了性能"作为理由)。
[ ] cold-first-navigation: 至少 --cold-iterations 10,前后数字均有
[ ] repeated mean / median / P95: 同 warmup+iterations 跑前后
[ ] 单进程/单 sample 数据明确标记为 smoke-only,不作为最终结论
[ ] 测量场景对应的 Gallery ShowCase: ___________________[ ] 一行 git command 能从 merged tip 回退:
git revert <sha> 或 git checkout <sha>~1 -- <files>
[ ] 改动文件范围 + 文件数: ___ 个文件 / ___ 个目录
[ ] 范围足够小,回退是机械操作。[ ] 拥有项目构建通过(无新增 warning)
[ ] git diff --check 干净
[ ] 没有遗留未使用的 using 指令Every completed optimization must include a user-facing benefit table in the final response. Do not wait for the user to ask "汇报收益".
[ ] 先给一句话结论,用直观语言说明「减少了什么 / 少了多少 / 哪个用户场景受益」
[ ] 最终回复包含收益表,列为: metric / baseline / optimized / formula / improvement / conclusion
[ ] metric 使用用户能看懂的名称和单位,不只写内部类名、方法名、callsite count
[ ] conclusion 说明影响场景,例如每次关闭、每行生成、每个 DataGrid、Gallery 页面加载
[ ] baseline 与 optimized 的口径一致;若是结构收益,单位写清楚(per control / per item / per root / Gallery estimated total)
[ ] 百分比使用 `(baseline - optimized) / baseline`;对 "removed" 类指标也给出百分比
[ ] 结构收益必须翻译成直观说法,例如「每次操作少创建 X 个对象」「每个实例少 Y 个订阅」「每次 arrange 少 Z 个 Geometry」
[ ] timing 数据若只是单次 smoke,明确标记 smoke-only,不当作确定性能提升
[ ] smoke-only timing 单独标明,不混入确定收益;结构优化不能用单次 timing 包装成确定速度提升
[ ] 若没有可靠 timing before/after,明确说明不声明 timing 百分比,只声明结构/分配/正确性收益
[ ] 正确性修复、验证命令、未能验证的残余风险一并汇报Each control with non-trivial behavior must maintain a matrix at tools/performances/AtomUI.Performance/Suites/<Control>/Regression.md. Every perf commit on that control must declare which matrix entries it ran.
Minimal matrix shape:
# <Control> Regression Matrix
## Functional matrix (must run before any perf commit on this control)
- [ ] <feature 1 + interaction>
- [ ] <feature 2 + interaction>
- [ ] ...
## Multi-step user flows (Gallery ShowCase scripts)
- [ ] <ShowCase A>: open → step 1 → step 2 → expected outcome
- [ ] <ShowCase B>: ...
## Lifecycle matrix
- [ ] Mount → unmount → re-mount with state preserved
- [ ] Re-template
- [ ] Property toggle on/off/on<Control>StateVerification.cs cases must map 1:1 with matrix items where automatable. Items that can only be verified by visual inspection (animation, hover transition, popup arrow position) must be listed and the reviewer must walk them in Gallery, with timestamps in the commit message.
When a single optimization pattern is being applied across multiple controls, this is a series planning concern, not just a per-commit one.
docs/superpowers/progress/<date>-<pattern>-rollout-audit.md.IsSet(property) as "user explicitly set this". In Avalonia, IsSet returns true for any effective styled value or binding (Template/Style/Animation/LocalValue alike). Using it as a "user wrote this" check causes silent default restoration failures.LocalValue and Animation must always be stronger than internal defaults. If user sets a local value, internal token binding must be disposed or not observed as the effective value.ItemSpacing / LineSpacingSpace optimization changed internal token spacing bindings without fully modeling the Gallery example:
<atom:Space SizeType="{Binding SizeType}"
LineSpacing="{Binding #CustomSizeSlider.Value, Priority=Template}"
ItemSpacing="{Binding #CustomSizeSlider.Value, Priority=Template}" />First fix moved internal bindings to BindingPriority.Style, avoiding same-priority disposal collisions but creating a new bug: the always-present slider Template binding won in Small/Middle/Large, so those SizeType options stopped changing spacing. Only Custom still worked, masking the bug unless the full interaction was tested.
Lesson: Whenever you touch a property that has both internal default and external override, write down the priority of every binding on that property before changing anything.
A private bool that short-circuits a property-changed handler is the single most reliable indicator that the event flow is wrong. The pattern surfaced repeatedly in the rolled-back Pattern A commits (_ignoreSelectedPropertyChanged, IgnorePropertyChange = true, IsPlayingCloseMotion-driven cancels). Each occurrence masked a real ordering bug; removing the flag during rollback exposed the bug, and fixing the flow — not the flag — solved it.
When a property-changed handler is about to set another property whose own handler will set the first property again, do NOT add an ignore flag. Walk the loop and break it at the right link.
BeginUpdate() / EndUpdate() style, where the suppression is bounded by code structure (a using scope, a try/finally), not by a bool field that lives between events.bool is acceptable only when both entry and exit are deterministic (set in path A, cleared by exactly one event in path B that is guaranteed to run). The Cascader incident proved that "cleared in OnSomethingFinished" is not deterministic enough — async paths skip it.| Loop shape | Fix |
|---|---|
| User sets A → handler-of-A sets B → handler-of-B sets A again | The second SetValue should be SetCurrentValue (preserves binding source, no priority bump). Or the second handler should if (!equals) Set.... Both break the loop without a flag. |
| Internal token binding writes default → user binding overrides → internal writes again | Move internal binding to a different BindingPriority frame. See Avalonia Binding Priority Guardrails. |
| Pre-state must be synchronized before the visible change | Split into a dedicated IObservable / pre-event so order is explicit. The original change becomes "publish pre, then change", not "change with a flag-guard around the recursion". |
| Animation cancel recursing into open/close | Animation cancel should complete (or Stop without raising completion), not write the IsOpen property again. If the animation API forces a write-back, factor open/close into a small state machine with named transitions, then drive transitions from one place. |
Grep the diff for new _ignore, _suppress, IgnorePropertyChange, _isUpdating, _isHandling, IsPlayingCloseMotion-style fields. Each one is presumed to violate this rule until its commit description explains which acceptable case it falls under. "We've always had similar flags" is not an explanation.
_ignoreSelectedPropertyChanged deadlock. A flag added to "stop the recursion" was set in path A but never cleared on async path B's exit. The control ended up permanently muted; selection didn't sync, popup couldn't reopen, leaf clicks didn't close.IsPlayingCloseMotion + IgnorePropertyChange = true cancel. Together they caused all popup-bearing controls to fail to reopen reliably. The rollback restored the older event flow that did not need either flag.For any control performance optimization:
tools/performances/AtomUI.Performance/Suites/<Control>/Regression.md if the matrix needs new entries.<Control>StateVerification.cs with cleanup assertions for any visuals/presenters/hosts/subscriptions/bindings created.controlgallery/AtomUIGallery.Desktop/AtomUIGallery.Desktop.csproj, walk the matrix's "Multi-step user flows" entries by hand, record results.git diff --check; ensure no unused using.Performance summaries must be readable to a human reviewer.
Scenario, Before, After, Improvement, units in every value.Before writing any C# code that creates / subscribes / captures, list the pair upfront. Every left-hand call must have a defined right-hand call AND a definite event that fires it. The Resource Leak Detection grep below is the post-hoc audit; this is the up-front design step.
| Create / Acquire | Release | Where the release fires |
|---|---|---|
_handler = Handle...; foo.Bar += _handler; | foo.Bar -= _handler; | OnDetachedFromVisualTree. For lazy materialization paths, the symmetric Clear*. |
child.SetTemplatedParent(this); Children.Add(child); | Children.Remove(child); child.SetTemplatedParent(null); | Symmetric Clear* when lazy; otherwise OnDetachedFromVisualTree. |
_disposables ??= new(); _disposables.Add(target.Bind(...)); | _disposables.Dispose(); _disposables = null; | OnDetachedFromVisualTree. Re-templating: also at the top of the next OnApplyTemplate. |
pointer.Capture(this); | pointer.Capture(null); | The terminating gesture event (PointerReleased, PointerCaptureLost). Never on a timer. |
_timer = new DispatcherTimer(...); _timer.Tick += ...; _timer.Start(); | _timer.Stop(); _timer.Tick -= ...; _timer = null; | OnDetachedFromVisualTree AND any explicit "stop" path. |
_topLevel.Deactivated += ...; (any TopLevel / Window-scoped subscription) | matching -= | OnDetachedFromVisualTree. The TopLevel can outlive the control by far. |
EnableTransitions(); | DisableTransitions(); | Around any block that mutates animatable properties before attach. Must be paired even if the mutation throws — use try/finally. |
_popupHost.Open(); | _popupHost.Close(); | Detach, re-template, or explicit close. The popup host is a separate visual root and will not auto-close on owner detach unless told. |
Clear* is mandatory for lazy materialization. Anything an EnsureXxx() creates, ClearXxx() must release in reverse order. If the creation order is bind A → wire event B → add child C, the clear order is remove C → unwire B → dispose A.OnDetachedFromVisualTree is the catch-all release point. Any subscription that is global / TopLevel-scoped / cross-control MUST release here regardless of any other Clear* path. Clear* covers the lazy-materialization case; detach covers the "control was attached, control is gone" case. Both must be wired.CompositeDisposable, disposed at the START of the next OnApplyTemplate and at OnDetachedFromVisualTree. Storing them as plain fields and forgetting on re-template is a known leak shape.Tier 1 §3 says "anything created/subscribed/bound/cached/lazily materialized/reparented must have a defined and verified release path before the change is considered complete". This table is the operational form. The Resource Leak Detection grep is the audit form.
Run before every perf commit and paste results in Gate 2. Any non-empty output requires explanation.
# Newly added Ensure*/Clear* must be paired
rg "Ensure[A-Z]\w+\(\)" --type cs -A 3 src/
# Each += event subscription needs matching -=
rg "\\+= Handle" --type cs src/
# CompositeDisposable usage — confirm each one is required by lifetime mismatch
rg "CompositeDisposable\\b" --type cs src/
# Children.Add / Insert without matching Remove
rg "Children\\.(Add|Insert)" --type cs src/
# Disposable fields — each must have a release path
rg "_[a-z]\\w+Disposables\\b" --type cs src/
# SetTemplatedParent(this) calls — each should have a matching SetTemplatedParent(null)
rg "SetTemplatedParent\\(this\\)" --type cs src/
# DisableTransitions/EnableTransitions pairing
rg "DisableTransitions\\(\\)|EnableTransitions" --type cs src/Use these to ground claims in numbers, not guesses.
LayoutManager.LayoutUpdated event count: how many layout passes a user action triggered.Renderer.SceneInvalidated count: invalidate calls reaching the render thread.Avalonia.Diagnostics.DevTools (F12 in dev): visual tree, property values, binding state, layout overlay.OnPropertyChanged to measure real change frequency.BindingDiagnostics.IsLoggingEnabled = true (in dev builds) for binding failure warnings.dotnet-counters monitor -p <pid>: real-time GC / allocation rate.var count = 0; visual.VisitDescendants(_ => count++); for node count.Do not report "assumed" cost savings. Every cost claim must include:
BindUtils.RelayBind(...), storing an IDisposable binding, or adding a CompositeDisposable: first confirm Avalonia [!] binding is insufficient (i.e., source/target lifetimes differ).AtomUI.Utils.MathUtils (AreClose, IsZero, GreaterThan, LessThanOrClose, etc.) for floating-point comparison, epsilon checks, zero/one checks, ordering with tolerance, angle conversion, fixed-point rounding. Do not introduce hand-written Math.Abs(...) < eps, new epsilon constants, or duplicate helpers.Dispatcher.Post(this.EnableTransitions); — method-group form. Do not wrap in lambda.Debug.Assert(value != null) immediately followed by a nullable guard for the same value. Express the invariant in the type, helper return value, or use a real runtime guard with explicit recovery.using directives introduced by the change.Real bugs that cost the project significant rollback effort. New incidents must be appended here with a one-paragraph summary and a "what to check next time" line.
See Avalonia Binding Priority Guardrails — Incident above. Lesson: when changing token-default bindings, write down all bindings on that property and their priorities before editing.
A series of perf commits applied "theme element → C# dynamic creation" pattern (Pattern A) across ~50 controls. Side effects:
_ignoreSelectedPropertyChanged flag deadlocked._cascaderView.OptionsSource = Options.Cast<...>().ToList() on every property change triggered Remove notifications on the underlying _options collection, silently wiping SelectedOptions in multi mode. User saw filter input keystrokes erase their tag selections.IsPlayingCloseMotion cancel + IgnorePropertyChange = true; SetCurrentValue(IsDropDownOpenProperty, false) made all popup controls fail to reopen reliably.IsHorizontalFlipped="{Binding $parent[atom:Popup].IsHorizontalFlipped}" with a C# SetCurrentValue chain broke the arrow-tracks-active-input behavior in IsShowTime mode.GlobalIndexFromContainer instead of IndexFromContainer + Items[index] caused clicks under filter to select the wrong item.The rollback ultimately squashed 50+ commits into a single revert and restored the OLD axaml-based architecture. Lesson: Pattern A's complexity cost (manual SetCurrentValue chains, disposable bookkeeping, race-prone state machines) consistently outweighed its allocation savings. This is the original justification for the Theme Static Rule.
When evaluating a "looks like Pattern A" perf proposal, immediately ask:
IsVisible="False" + style selector achieve the same conditional-cost outcome? (Avalonia.Base/Layout/Layoutable.cs:546, :671 confirm it's free.)ItemsControl realization), or only a few hundred bytes / one Visual instance? Past rollback evidence: a few hundred bytes per item never paid for the complexity churn./template/ style rules, are you ready to call SetTemplatedParent(this) on creation and SetTemplatedParent(null) on teardown? TemplateSelector.Evaluate returns NeverThisInstance without it (Avalonia.Base/Styling/TemplateSelector.cs:39-49).If (1) is yes, or (2) is "only a few hundred bytes", or (3) is "only once", or (4) is "yes, multiple handlers", or (5) is "we forgot about that" — the optimization should not be Pattern A.
© AtomUI, LGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .agents/skills/atomui-control-performance of AtomUI/AtomUI.
Open the folder on GitHubat commit d234fe0
Atomui Control Performance next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Atomui Control Performance this skillAtomUI/AtomUI | 842 | — | ~17k | Automated safety check: Pass | LGPL-3.0 | |
| Docs Style LintAvaloniaUI/avalonia-docs | 166 | — | ~4.6k | Automated safety check: Pass | None | |
| Wfa Harden Decomposetautcony/ChapterTool | 113 | — | ~1.1k | Automated safety check: Pass | GPL-3.0 | |
| Winforms To Avaloniatautcony/ChapterTool | 113 | — | ~1.2k | Automated safety check: Pass | GPL-3.0 | |
| Fieldworks Code Commentingsillsdev/FieldWorks | 110 | — | ~2.4k | Automated safety check: Pass | Custom licence | |
| Convert Slicesillsdev/FieldWorks | 110 | — | ~3.3k | Automated safety check: Pass | Custom licence |
AvaloniaUI/avalonia-docs
Reviews and lints Avalonia documentation pages against house style rules, content boundaries, anti-marketing standards, accessibility, and SEO.
tautcony/ChapterTool
Phase G of a WinForms-to-Avalonia migration: decompose oversized UI state and orchestration, harden asynchronous workflows and external boundaries, preserve composition lifetimes, keep Headless…
tautcony/ChapterTool
Orchestrate a phased WinForms-to-Avalonia migration: behavior contracts, UI-independent application logic, platform adapters, MVVM shell, parity/cutover, evolution, and hardening.
sillsdev/FieldWorks
The FieldWorks code-comment standard for C, C/C++, IDL, PowerShell, and project-file/Avalonia-view XML comments.
sillsdev/FieldWorks
Drive the conversion of one legacy slice type to the Avalonia detail view through analysis, developer alignment, integration-test planning, route/exemplar mapping, design, and scaffold/implement.
tautcony/ChapterTool
Phase B of a WinForms-to-Avalonia migration: extract UI-independent application behavior, state rules, use cases, and boundary contracts with TDD and no WinForms/Avalonia references.
AtomUI/AtomUI
A skill your agent uses when building, launching, debugging, or visually inspecting AtomUIGallery.Desktop from an AtomUI checkout where installed or mounted Gallery apps may share its name or bundle…
AtomUI/AtomUI
Generate a single-line commit message for AtomUI by reading the project's git staged area and recent commit style.
AtomUI/AtomUI
A skill your agent uses when optimizing, refactoring, reviewing, or fixing AtomUI controls, including control API contracts, member layout, file splitting, lifecycle, AXAML structure, correctness…
AtomUI/AtomUI
A skill your agent uses when creating, completing, splitting, reviewing, or synchronizing AtomUI control documentation under docs/controls, including overview.md, implementation.md, token.md…
AtomUI/AtomUI
A skill your agent uses when changing AtomUI or Avalonia resource bindings, DynamicResource, TokenResourceBinder, non-Visual AvaloniaObject lifecycle, IResourceHost/IThemeVariantHost…
AtomUI/AtomUI
A skill your agent uses when upgrading AtomUI NuGet dependencies, source dependency projects, ReactiveUI, Avalonia, Splat, or any third-party version where compatibility must be evaluated before…
Works with
Categories
A skill your agent uses when optimizing AtomUI controls, investigating control performance regressions, changing Avalonia styled-property bindings, lazy creation, templates, selectors, or Gallery…. Atomui Control Performance is an agent skill from AtomUI/AtomUI. Use when optimizing AtomUI controls, investigating control performance regressions, changing Avalonia styled-property bindings, lazy creation, templates, selectors, or Gallery performance scenarios.
Atomui Control Performance fits situations like: optimizing AtomUI controls; investigating control performance regressions; changing Avalonia styled-property bindings; gallery performance scenarios.
Run `npx skills add AtomUI/AtomUI --skill atomui-control-performance -a claude-code`. Or copy the skill folder (.agents/skills/atomui-control-performance in AtomUI/AtomUI) into .claude/skills/atomui-control-performance in your project. Claude Code loads it when a task matches its description.
Run `npx skills add AtomUI/AtomUI --skill atomui-control-performance -a codex`. Or copy the skill folder (.agents/skills/atomui-control-performance in AtomUI/AtomUI) into .agents/skills/atomui-control-performance in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add AtomUI/AtomUI --skill atomui-control-performance -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/atomui-control-performance, .gemini/skills/atomui-control-performance, .github/skills/atomui-control-performance and .opencode/skills/atomui-control-performance in your project.
Going by SKILL.md and its folder, Atomui Control Performance needs the command-line tools its instructions call (rg, dotnet and git).
SKILL.md contains no URLs. Its commands use git, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Atomui Control Performance is published under the LGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 17k tokens (SKILL.md is roughly 68k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Atomui Control Performance: Docs Style Lint (AvaloniaUI/avalonia-docs, 166 stars), Wfa Harden Decompose (tautcony/ChapterTool, 113 stars), Winforms To Avalonia (tautcony/ChapterTool, 113 stars) and Fieldworks Code Commenting (sillsdev/FieldWorks, 110 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
AtomUI (a GitHub organization) maintains it in AtomUI/AtomUI, which has 842 GitHub stars. The repository holds 15 skills in this directory. The repository was last updated on October 7, 2026.
Source: AtomUI/AtomUI on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.