Skip to content

Cache Brushes - #36405

Merged
kubaflo merged 21 commits into
dotnet:net11.0from
pictos:pj/cache-brushes
Jul 31, 2026
Merged

Cache Brushes#36405
kubaflo merged 21 commits into
dotnet:net11.0from
pictos:pj/cache-brushes

Conversation

@pictos

@pictos pictos commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Issues Fixed

Fixes #35302

Description of Change

This pull request introduces a new caching mechanism for ImmutableBrush instances, specifically optimizing color-to-brush conversions in the Brush class. It adds a two-stage cache (simple and LRU) to reduce allocations and improve performance when repeatedly converting colors to brushes. Additionally, a suite of benchmarks is included to measure the impact of these changes on property-change propagation in common controls.

Caching improvements for brush creation:

  • Introduced a new CacheWithSwitch class that provides a two-stage caching strategy for ImmutableBrush instances keyed by Color. It starts with a simple dictionary cache and automatically promotes to an LRU cache when capacity is reached, reducing memory allocations and improving brush reuse. (src/Controls/src/Core/Internals/CacheWithSwitch.cs)
  • Added an ICache<TKey, TValue> interface to standardize cache implementations used for brush caching. (src/Controls/src/Core/Internals/ICache.cs)
  • Implemented an LRUBrushCache class for least-recently-used caching of brushes, used as the second stage of CacheWithSwitch. (src/Controls/src/Core/Internals/LRUBrushCache.cs)

Integration with Brush class:

  • Updated the Brush class to use the new cache for implicit conversions from Color and SolidPaint, ensuring brush instances are reused whenever possible. (src/Controls/src/Core/Brush/Brush.cs) [1] [2] [3]

Benchmarking and performance validation:

  • Added a new benchmark suite to measure property-change propagation performance and allocations for Label, Button, and Entry controls, with a focus on background color changes that exercise the new brush caching logic. (src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs)

Brush cache benchmark: branch vs main

Setup: main has no brush cache — every Paint → Brush conversion allocates new SolidColorBrush { Color = c }. This branch adds Lru64ColorVectorInlineBrushCache (inline-array + SIMD lookup) and wires it into Brush.cs. Benchmarked over identical color sets, ShortRun, Apple M4 Max.

Caveat: main targets net10 (ran on .NET 10.0.9) and this branch targets net11 (ran on .NET 11.0-preview.6) — the runtimes differ because the TFMs differ. As a control, the branch NullCache (allocate-every-time, ~68 ns / 1024 B) closely matches main's no-cache (~84 ns / 1116 B), so the runtime gap is small (~15 ns) and the cache wins are real.

Production path — InlineLruCache (branch) vs no-cache (main)

Scenario main (no cache) branch InlineLruCache Time Δ main alloc branch alloc Alloc Δ
40 colors (fits in cap 50) 84.36 ns 16.92 ns ≈5.0× faster (−80%) 1116 B 105 B −90.6%
Weighted (20 hot ×10 + 40 cold) 80.53 ns 24.88 ns ≈3.2× faster (−69%) 1116 B 180 B −83.9%
60 colors (> cap 50, thrashes) 84.24 ns 87.06 ns ≈even (+3%) 1116 B 1024 B −8%

Full branch run (net11, ShortRun)

Scenario LruCache (dict+LL) InlineLruCache (new) NullCache (≈main)
40 colors 36.87 ns / 119 B 16.92 ns / 105 B 68.19 ns / 1024 B
60 colors 137.85 ns / 1.05 KB 87.06 ns / 1 KB 75.61 ns / 1 KB
Weighted 36.77 ns / 190 B 24.88 ns / 180 B 68.02 ns / 1024 B

Takeaways

  • vs main: for realistic workloads (colors fit in the cap), the new cache is 3–5× faster and allocates ~6–11× less — main allocates a full SolidColorBrush (~1 KB) on every conversion, the cache returns a shared instance.
  • vs the old LRU in this PR: InlineLruCache is also 1.5–2.1× faster than the dict+linked-list LRUBrushCache.
  • The 60-color row is pathological: 60 distinct colors exceed the capacity-50 benchmark, so it thrashes (every access misses + allocates). In production the cap is 64 and typical apps reuse a handful of colors, so the 40-color / weighted rows are representative.

Copilot AI review requested due to automatic review settings July 6, 2026 02:25
@pictos
pictos had a problem deploying to copilot-pat-pool July 6, 2026 02:25 — with GitHub Actions Failure
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36405

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36405"

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label Jul 6, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@pictos! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @pictos! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request targets issue #35302 by reducing SolidColorBrush/ImmutableBrush churn on hot PropertyChanged paths, primarily by caching color-to-brush conversions in Brush implicit operators, and by adding benchmarks to quantify the allocation/runtime impact.

Changes:

  • Added a two-stage Color -> ImmutableBrush cache (simple dictionary that promotes to a small LRU) under Microsoft.Maui.Controls.Internals.
  • Updated Brush implicit conversions from Color and SolidPaint to reuse cached ImmutableBrush instances.
  • Added BenchmarkDotNet benchmarks focused on property-change propagation and background/brush assignment allocations.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/Controls/src/Core/Brush/Brush.cs Switches implicit conversions to cached ImmutableBrush instances (allocation/perf optimization).
src/Controls/src/Core/Internals/ICache.cs Introduces a small internal cache abstraction for brush caching.
src/Controls/src/Core/Internals/CacheWithSwitch.cs Implements a two-stage cache (simple → LRU) for ImmutableBrush keyed by Color.
src/Controls/src/Core/Internals/LRUBrushCache.cs Adds a fixed-capacity LRU cache implementation used after promotion.
src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs Adds benchmarks to measure property-change propagation performance/allocations (esp. background changes).

Comment thread src/Controls/src/Core/Brush/Brush.cs
Comment thread src/Controls/src/Core/Brush/Brush.cs Outdated
Comment thread src/Controls/src/Core/Brush/Brush.cs Outdated
Comment thread src/Controls/src/Core/Internals/LRUBrushCache.cs Outdated
Comment thread src/Controls/src/Core/Internals/LRUBrushCache.cs Outdated
Copilot AI review requested due to automatic review settings July 6, 2026 02:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread src/Controls/src/Core/Internals/CacheWithSwitch.cs Outdated
Comment thread src/Controls/src/Core/Internals/LRUBrushCache.cs
Copilot AI review requested due to automatic review settings July 6, 2026 02:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread src/Controls/src/Core/Brush/Brush.cs
Comment thread src/Controls/src/Core/Brush/Brush.cs
Comment thread src/Controls/src/Core/Internals/CacheWithSwitch.cs Outdated
Comment thread src/Controls/tests/Core.UnitTests/CacheWithSwitchUnitTests.cs Outdated
Copilot AI review requested due to automatic review settings July 6, 2026 02:41
@pictos
pictos force-pushed the pj/cache-brushes branch from b7a3949 to 30c6f0d Compare July 6, 2026 02:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Comment on lines +24 to +29
public LRUBrushCache(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);

_capacity = capacity;
}
Comment on lines +31 to +44
public LRUBrushCache(int capacity, Dictionary<Color, ImmutableBrush> brushes)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
ArgumentNullException.ThrowIfNull(brushes);

if (brushes.Count > capacity)
{
throw new ArgumentException("Brush count must not exceed capacity.", nameof(brushes));
}

_capacity = capacity;
_cache = new Dictionary<Color, LinkedListNode<ImmutableBrush>>(capacity);
_lru = [];

Comment on lines +12 to +17
public CacheWithSwitch(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);

_cache = new SimpleCache(capacity);
}

private class SimpleCache(int capacity) : ICache<Color, ImmutableBrush>
{
readonly Dictionary<Color, ImmutableBrush> _dict = [];
Comment on lines 19 to +23
if (paint is SolidPaint solidPaint)
return new SolidColorBrush { Color = solidPaint.Color };
{
var color = solidPaint.Color;
return color is null ? Default : _cache.Get(solidPaint.Color);
}
/// Properties tested:
/// - HeightRequest (double)
/// - Background (Brush – exercises color-to-brush conversion and caching)
/// - IsEnabled (bool – coerced through the visual tree, uses boxed value caching)
Comment on lines +21 to +22
/// The Background property is particularly interesting as it exercises implicit Color-to-Brush
/// conversion, which can benefit from brush instance caching. IsEnabled tests boxed bool reuse.
Copilot AI review requested due to automatic review settings July 6, 2026 02:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment on lines 19 to +23
if (paint is SolidPaint solidPaint)
return new SolidColorBrush { Color = solidPaint.Color };
{
var color = solidPaint.Color;
return color is null ? Default : _cache.Get(solidPaint.Color);
}
Comment on lines +45 to +49
foreach (var (color, brush) in brushes)
{
var node = _lru.AddFirst(brush);
_cache.Add(color, node);
}
Comment on lines +15 to +19
/// - HeightRequest (double)
/// - Background (Brush – exercises color-to-brush conversion and caching)
/// - IsEnabled (bool – coerced through the visual tree, uses boxed value caching)
/// - Opacity (double – coerced to [0,1])
/// - FontSize (double – Button only)
@kubaflo

kubaflo commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@pictos I love this PR like all the performance ones!

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 6, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — 3 findings

See inline comments for details.


public ImmutableBrush Get(Color key)
{
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(_dict, key, out _);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

[critical] Build / netstandard compatibilityControls.Core.csproj still targets netstandard2.0/netstandard2.1, but this new unconditional code uses APIs that are not available in the netstandard reference assemblies (CollectionsMarshal.GetValueRefOrAddDefault here, plus ThrowIfNegativeOrZero/ThrowIfNull in the new cache types). Existing code such as BindableObject.GetOrCreateContext guards CollectionsMarshal with #if !NETSTANDARD; this needs the same fallback or netstandard-safe dictionary code.

Comment thread src/Controls/src/Core/Brush/Brush.cs Outdated
[System.ComponentModel.TypeConverter(typeof(BrushTypeConverter))]
public abstract partial class Brush : Element
{
static readonly ICache<Color, ImmutableBrush> _cache = new CacheWithSwitch(51);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

[major] Threading/static state — This makes every public Color/SolidPaint to Brush conversion mutate one process-wide cache, but the cache implementation uses Dictionary/LinkedList and explicitly documents LRUBrushCache as not thread-safe. The previous conversion was stateless. Concurrent conversions can corrupt the cache or throw; make the static cache synchronized/thread-safe or keep the conversion allocation-local.

public static Brush Default => defaultBrush ??= new(null);

public static implicit operator Brush(Color color) => new SolidColorBrush(color);
public static implicit operator Brush(Color color) => color is null ? Default : _cache.Get(color);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

[major] Public behavior change — The implicit Color conversion used to return a fresh mutable SolidColorBrush; it now returns a cached ImmutableBrush. Code such as var b = (SolidColorBrush)(Brush)Colors.Red; b.Color = Colors.Blue; used to update that independent brush, but now silently no-ops and shares the instance globally. Avoid changing the mutability contract of this public conversion, or explicitly account for the breaking behavior with API review/tests.

@MauiBot MauiBot added s/agent-gate-failed AI could not verify tests catch the bug s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels Jul 6, 2026
Copilot AI review requested due to automatic review settings July 31, 2026 01:42
@pictos
pictos force-pushed the pj/cache-brushes branch from c578936 to e351aa4 Compare July 31, 2026 01:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

src/Controls/src/Core/Brush/Brush.cs:24

  • In the SolidPaint -> Brush implicit conversion, solidPaint.Color is read into a local variable (color) but the cached lookup uses solidPaint.Color again. Using the local variable avoids an extra property access and guarantees consistent behavior if Color were to change between reads.
				var color = solidPaint.Color;
				return color is null ? Default : _cache.Get(solidPaint.Color);
			}

src/Controls/src/Core/Brush/Brush.cs:112

  • The Color/SolidPaint -> Brush implicit conversions now return cached ImmutableBrush instances (instead of allocating a new SolidColorBrush). This is an observable behavior change: callers may now receive a shared, effectively immutable brush instance (e.g., ((SolidColorBrush)brush).Color = ... is a no-op due to ImmutableBrush.Color override). If this is intentional, it would be good to explicitly lock in/validate the new contract (e.g., additional tests around mutability/reference reuse) to avoid regressions; if not, the cache may need to be internal-only to the hot path rather than exposed via the public implicit operators.
		public static Brush Default => defaultBrush ??= new(null);

		public static implicit operator Brush(Color color) => color is null ? Default : _cache.Get(color);

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:57

  • This benchmark file introduces long dashed "section divider" comments (e.g., // -------------------------------------------------------------------------). The Benchmarks folder generally uses shorter single-line section headers (e.g., // --- #NNNNN: ... ---), and the long separators add noise without providing extra information. Consider replacing them with a simple one-line header comment (and apply the same change to the similar block around the Background benchmarks).
		// -------------------------------------------------------------------------
		// HeightRequest property (double)
		// -------------------------------------------------------------------------

Copilot AI review requested due to automatic review settings July 31, 2026 02:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (7)

src/Controls/src/Core/Brush/Brush.cs:24

  • In the SolidPaint conversion, you already store solidPaint.Color in a local variable but then call the property again for the cache lookup. Using the local color avoids a redundant property access and guarantees the value you null-checked is the value used as the cache key.
			if (paint is SolidPaint solidPaint)
			{
				var color = solidPaint.Color;
				return color is null ? Default : _cache.Get(solidPaint.Color);
			}

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:83

  • Avoid banner-style separator comments; they add noise and are discouraged in this repo’s guidelines. A simple one-line header comment is enough here.
		// -------------------------------------------------------------------------
		// Background property (Color)
		// -------------------------------------------------------------------------

src/Controls/src/Core/Internals/LRUBrushCache.cs:64

  • _cache/_lru are initialized at the field declaration and re-assigned in the seeded constructor, which means the seeded constructor pays for two allocations that are immediately discarded. Consider removing the field initializers and always initializing the fields explicitly in the constructors.
	readonly Dictionary<Color, LinkedListNode<ImmutableBrush>> _cache = [];
	readonly LinkedList<ImmutableBrush> _lru = [];
	readonly int _capacity;

src/Controls/src/Core/Internals/LRUBrushCache.cs:32

  • After removing the field initializers for _cache/_lru, the primary constructor should initialize them (and can pre-size the dictionary) so the instance is usable when constructed without seeding.
	public LRUBrushCache(int capacity)
	{
		if (capacity <= 0)
		{
			throw new ArgumentOutOfRangeException(nameof(capacity));
		}

		_capacity = capacity;
	}

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:4

  • using System; is unused in this benchmark file.
using System;
using BenchmarkDotNet.Attributes;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:58

  • Avoid banner-style separator comments; they add noise and are discouraged in this repo’s guidelines. A simple one-line header comment is enough here.

This issue also appears on line 80 of the same file.

		// -------------------------------------------------------------------------
		// HeightRequest property (double)
		// -------------------------------------------------------------------------

src/Controls/src/Core/Brush/Brush.cs:4

  • using System; appears unused in this file; leaving it in can trigger IDE0005 (or similar analyzers) and adds noise.
#nullable disable
using System;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;

Copilot AI review requested due to automatic review settings July 31, 2026 03:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (7)

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:6

  • This benchmarks project consistently uses the Microsoft.Maui.Handlers.Benchmarks namespace (e.g., src/Core/tests/Benchmarks/Program.cs:3). Using Microsoft.Maui.Benchmarks here is inconsistent and makes it harder to locate related benchmarks.
namespace Microsoft.Maui.Benchmarks

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:57

  • The long dashed separator comment blocks add noise and don’t match the existing style in this benchmarks project (which typically uses short // --- section headers). Consider removing the dashed lines and keeping just the one-line section header.
		// -------------------------------------------------------------------------
		// HeightRequest property (double)
		// -------------------------------------------------------------------------

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:82

  • Same as above: consider removing the dashed separator lines and keeping only the one-line section header.
		// -------------------------------------------------------------------------
		// Background property (Color)
		// -------------------------------------------------------------------------

src/Controls/src/Core/Internals/LRUBrushCache.cs:65

  • LRUBrushCache(int capacity, Dictionary<Color, ImmutableBrush> brushes) seeds the LRU list with brush instances but later evicts entries by removing _cache using last.Value.Color as the key. If any seeded brush’s Color doesn’t exactly match its dictionary key (or a seeded value is null), eviction can fail to remove the correct key and corrupt the cache state. Add validation while seeding to ensure the brush instance is non-null and its Color matches the key.
		foreach (var pair in brushes)
		{
			var color = pair.Key;
			var brush = pair.Value;
			var node = _lru.AddFirst(brush);

src/Controls/src/Core/Internals/InlineLRUCache/Lru64ColorVectorInlineBrushCache.cs:4

  • using System; is unused in this file.
#nullable disable

using System;
using Microsoft.Maui.Graphics;

src/Core/tests/Benchmarks/Benchmarks/PropertyChangePropagationBenchmarker.cs:4

  • using System; is unused in this file.

This issue also appears in the following locations of the same file:

  • line 6
  • line 55
  • line 80
using System;
using BenchmarkDotNet.Attributes;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

src/Controls/src/Core/Brush/Brush.cs:16

  • The PR description references a CacheWithSwitch two-stage cache and a CacheWithSwitch.cs file, but the current implementation wired into Brush uses Lru64ColorVectorInlineBrushCache directly and no CacheWithSwitch.cs exists in the repo. Please update the PR description to match the current approach and the files actually added/modified.
		static readonly ICache<Color, ImmutableBrush> _cache = new Lru64ColorVectorInlineBrushCache(64);

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 31, 2026
@@ -0,0 +1,299 @@
#nullable disable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it better to avoid this for new code?

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-gate-failed AI could not verify tests catch the bug s/agent-fix-win AI found a better alternative fix than the PR labels Jul 31, 2026
@MauiBot

MauiBot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

AI Review Summary

@pictos — new AI review results are available based on this last commit: 06aacbf.

Gate Passed Confidence Low Platform Android


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: ANDROID · Base: net11.0 · Merge base: eb1b76fb

Verified (new API / feature) — this PR adds new API and a test that references it in the same project, so reverting the fix un-compiles the test: there is no valid "fails without the fix" baseline to establish (a compile-coupled baseline). The gate instead verified the fix by a clean build + pass with the fix, so this is a real PASS rather than a non-committal INCONCLUSIVE.

Test Without Fix (expect FAIL) With Fix (expect PASS)
🧪 BrushTypeConverterUnitTests BrushTypeConverterUnitTests 🛠️ BUILD ERROR ✅ PASS — 50s
🧪 Lru64ColorVectorInlineBrushCacheUnitTests Lru64ColorVectorInlineBrushCacheUnitTests 🛠️ BUILD ERROR ✅ PASS — 20s
🧪 LRUBrushCacheUnitTests LRUBrushCacheUnitTests 🛠️ BUILD ERROR ✅ PASS — 19s
🔴 Without fix — 🧪 BrushTypeConverterUnitTests: 🛠️ BUILD ERROR · 80s

Error-relevant lines (filtered from the build log):

/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(209,20): error CS0246: The type or namespace name 'Lru64ColorVectorInline<>' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(227,20): error CS0246: The type or namespace name 'Lru64ColorVectorInline<>' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(21,57): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(27,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(38,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(49,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(59,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(75,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(94,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(113,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(129,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 BrushTypeConverterUnitTests: PASS ✅ · 50s

(no coded error found; showing last 1200 chars)

lorDefinition: "rgba(6, 201, 188, 0.2)") [< 1 ms]
  Passed TestBrushTypeConverterWithColorDefinition(colorDefinition: "rgba(100%, 32%, 64%,0.27)") [< 1 ms]
  Passed TestBrushBindingContext [17 ms]
  Passed TestBrushTypeConverterWithBrush(brush: "linear-gradient(90deg, rgb(255, 0, 0),rgb(255, 153"···) [2 ms]
  Passed TestBrushTypeConverterWithBrush(brush: "radial-gradient(circle, rgb(255, 0, 0) 25%, rgb(0,"···) [1 ms]
  Passed TestBrushTypeConverterWithColorHex(colorHex: "#ff00ff") [1 ms]
  Passed TestBrushTypeConverterWithColorHex(colorHex: "#00FF33") [< 1 ms]
  Passed TestBrushTypeConverterWithColorHex(colorHex: "#00FFff 40%") [< 1 ms]
  Passed ImplicitConversionFromNullColorReturnsEmptyBrush [1 ms]
  Passed ConvertNullTest [< 1 ms]
  Passed ImplicitConversionFromSolidPaintWithNullColorReturnsEmptyBrush [1 ms]
  Passed ImmutableBrushDoesntSetParent [< 1 ms]
  Passed TestGetGradientStopHashCode [< 1 ms]
  Passed TestBindingContextPropagation [< 1 ms]
[xUnit.net 00:00:01.38]   Finished:    Microsoft.Maui.Controls.Core.UnitTests
  Passed InvalidOperationExceptionWhenSettingParentOnImmutableBrush [< 1 ms]
Test Run Successful.
Total tests: 19
     Passed: 19
 Total time: 1.9825 Seconds
🔴 Without fix — 🧪 Lru64ColorVectorInlineBrushCacheUnitTests: 🛠️ BUILD ERROR · 24s

Error-relevant lines (filtered from the build log):

/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(21,57): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(27,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(38,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(49,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(209,20): error CS0246: The type or namespace name 'Lru64ColorVectorInline<>' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(227,20): error CS0246: The type or namespace name 'Lru64ColorVectorInline<>' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(59,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(75,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(94,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(113,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(129,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 Lru64ColorVectorInlineBrushCacheUnitTests: PASS ✅ · 20s

(no coded error found; showing last 1200 chars)

rsion=v11.0)
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 11.0.0-rc.1.26379.102)
[xUnit.net 00:00:00.36]   Discovering: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:03.85]   Discovered:  Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:03.88]   Starting:    Microsoft.Maui.Controls.Core.UnitTests
  Passed ReturnedBrushHasRequestedColor [29 ms]
  Passed ReturnsDifferentBrushInstancesForDifferentColors [< 1 ms]
  Passed RetainsIdentityForAllColorsUpToCapacity [2 ms]
  Passed CtorThrowsWhenCapacityIsOutOfRange(capacity: 0) [2 ms]
  Passed CtorThrowsWhenCapacityIsOutOfRange(capacity: 65) [< 1 ms]
  Passed CtorThrowsWhenCapacityIsOutOfRange(capacity: -1) [< 1 ms]
  Passed ReturnsSameBrushInstanceForSameColor [< 1 ms]
  Passed SupportsMaximumCapacityOf64 [< 1 ms]
  Passed RecentlyUsedColorSurvivesEviction [< 1 ms]
[xUnit.net 00:00:04.14]   Finished:    Microsoft.Maui.Controls.Core.UnitTests
  Passed ConcurrentAccessReturnsStableBrushIdentity [70 ms]
  Passed EvictsLeastRecentlyUsedEntryWhenFull [< 1 ms]
Test Run Successful.
Total tests: 11
     Passed: 11
 Total time: 5.1654 Seconds
🔴 Without fix — 🧪 LRUBrushCacheUnitTests: 🛠️ BUILD ERROR · 20s

Error-relevant lines (filtered from the build log):

/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(27,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(189,20): error CS0246: The type or namespace name 'Lru64ColorVectorInline<>' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(209,20): error CS0246: The type or namespace name 'Lru64ColorVectorInline<>' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(38,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(49,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(59,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(227,20): error CS0246: The type or namespace name 'Lru64ColorVectorInline<>' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(75,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(94,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(113,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Lru64ColorVectorInlineBrushCacheUnitTests.cs(129,20): error CS0246: The type or namespace name 'Lru64ColorVectorInlineBrushCache' could not be found (are you missing a using directive or an assembly reference?) [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 LRUBrushCacheUnitTests: PASS ✅ · 19s

(no coded error found; showing last 1200 chars)

r restore.
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net11.0/Microsoft.Maui.Controls.Core.UnitTests.dll (.NETCoreApp,Version=v11.0)
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.01] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 11.0.0-rc.1.26379.102)
[xUnit.net 00:00:00.32]   Discovering: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:03.15]   Discovered:  Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:03.17]   Starting:    Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:03.29]   Finished:    Microsoft.Maui.Controls.Core.UnitTests
  Passed LruBrushCacheCtorThrowsWhenCapacityIsNotPositive(capacity: -1) [11 ms]
  Passed LruBrushCacheCtorThrowsWhenCapacityIsNotPositive(capacity: 0) [2 ms]
  Passed LruBrushCacheEvictsLeastRecentlyUsedEntry [6 ms]
  Passed LruBrushCacheSeededCtorThrowsWhenBrushesAreNull [< 1 ms]
  Passed LruBrushCacheReturnsSameBrushForSameColor [1 ms]
  Passed LruBrushCacheSeededCtorThrowsWhenBrushCountExceedsCapacity [< 1 ms]
  Passed LruBrushCacheSeededCtorReusesSeededBrushInstances [< 1 ms]
Test Run Successful.
Total tests: 7
     Passed: 7
 Total time: 4.4175 Seconds

⚠️ Failure Details

  • 🛠️ BrushTypeConverterUnitTests without fix: build failed before tests could run
    • /home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/LRUBrushCacheUnitTests.cs(16,57): error CS0246: The type or namespace name 'LRUBrushCache' could not be found (are you missing a using directive o...
  • 🛠️ Lru64ColorVectorInlineBrushCacheUnitTests without fix: build failed before tests could run
    • /home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/LRUBrushCacheUnitTests.cs(16,57): error CS0246: The type or namespace name 'LRUBrushCache' could not be found (are you missing a using directive o...
  • 🛠️ LRUBrushCacheUnitTests without fix: build failed before tests could run
    • /home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/LRUBrushCacheUnitTests.cs(16,57): error CS0246: The type or namespace name 'LRUBrushCache' could not be found (are you missing a using directive o...
📁 Fix files reverted (1 files)
  • src/Controls/src/Core/Brush/Brush.cs

New files (not reverted):

  • src/Controls/src/Core/Internals/ICache.cs
  • src/Controls/src/Core/Internals/InlineLRUCache/Lru64.cs
  • src/Controls/src/Core/Internals/InlineLRUCache/Lru64ColorVectorInlineBrushCache.cs
  • src/Controls/src/Core/Internals/LRUBrushCache.cs

📱 UI Tests — Brush

Detected UI test categories: Brush

Deep UI tests — 42 passed, 0 failed across 1 category on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Brush 42/42 ✓
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

📋 Pre-Flight — Context & Validation

Issue: #35302 - Improve PropertyChanged performance scenarios
PR: #36405 - Cache Brushes
Platforms Affected: Android for requested testing; implementation is shared Controls/Core code affecting all platforms.
Files Changed: 5 implementation, 5 test/benchmark

Key Findings

  • Issue Improve PropertyChanged performance scenarios #35302 reports high SolidColorBrush allocation churn from implicit Color -> Brush conversions during common property-change scenarios.
  • PR Cache Brushes #36405 addresses the allocation path by changing public Color/SolidPaint -> Brush conversions to return cached ImmutableBrush instances from a process-wide LRU cache.
  • Gate artifacts already present for this run show Controls.Core unit tests passed with the PR fix and build-failed without the compile-coupled new cache types; gate verification was not re-run.
  • Prior review comments show earlier null-handling and thread-safety issues were addressed, but the public mutability/identity contract concern remains unresolved.

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: low
Errors: 1 | Warnings: 0 | Suggestions: 0

Key code review findings:

  • src/Controls/src/Core/Brush/Brush.cs:22 and src/Controls/src/Core/Brush/Brush.cs:110 — public Color/SolidPaint conversions now return shared immutable brushes instead of fresh mutable SolidColorBrush instances.
  • ✗ Prior MauiBot finding about the cached immutable conversion remains unresolved in the current diff.
  • Failure mode: code that casts the converted brush to SolidColorBrush and then sets Color used to mutate an independent brush; with the PR fix, the setter is a no-op on ImmutableBrush.
  • Blast radius: every public implicit Color/SolidPaint -> Brush conversion uses the static cache, so this affects all platforms and all controls using those conversions.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #36405 Process-wide LRU cache returning ImmutableBrush from public implicit conversions. ✅ PASSED (Gate) Brush.cs, cache files, unit tests, benchmarks Original PR; performant but changes public conversion mutability/identity semantics.

🔬 Code Review — Deep Analysis

Code Review — PR #36405

Independent Assessment

What this changes: Adds a process-wide cache for Color/SolidPaintBrush conversions, returning cached ImmutableBrush instances instead of allocating new SolidColorBrush objects. Also adds LRU cache implementations, tests, and benchmarks.

Inferred motivation: Reduce allocation churn in hot property-change/background paths.

Reconciliation with PR Narrative

Author claims: PR optimizes color-to-brush conversion allocation/performance for issue #35302.

Agreement/disagreement: The implementation matches the performance goal, but the narrative does not address the public behavioral change from fresh mutable brushes to shared immutable brushes. The PR body also appears stale in places, referencing CacheWithSwitch, which is not present in the current diff.

Prior Review Reconciliation

Prior ❌ Error Finding Source Status Evidence
Static cache was not thread-safe MauiBot inline review ✅ Fixed Current Lru64ColorVectorInlineBrushCache.Get locks around all cache access.
Color conversion now returns cached immutable brush MauiBot inline review ❌ Unresolved Current Brush.cs:110 still returns _cache.Get(color), which produces ImmutableBrush.
Added tests passed without fix / did not catch bug MauiBot gate reviews ❌ Unresolved Current tests cover null conversions and cache mechanics, but not the mutability/identity contract change or a failing pre-fix allocation regression.

External Output Contract

Consumer token/pattern Producer location Producer emission condition Consumer assumption Ordinary negative case Downstream effect
N/A N/A No changed code classifies external tool output with regex/string tokens. N/A N/A N/A

Blast Radius Assessment

  • Runs for all instances: Yes — every public implicit Color/SolidPaint to Brush conversion uses the static cache.
  • Startup impact: Low direct startup impact, but Brush type initialization now creates a static cache.
  • Static/shared state: Yes — one process-wide brush cache shared across all callers.
  • Public API behavior: High impact — conversion operators are public and widely used.

CI Status

  • Required-check result: gh pr checks --required could not run because gh is unauthenticated.
  • Fallback result: public check-runs show maui-pr failed on head 06aacbf7902fafa528b5a72549846fb3a36a114b.
  • Classification: failures appear in Samples integration builds with generated XAML nullability errors in Sandbox pages, not in changed brush/cache files; PR-causality is not established.
  • Action taken: loaded azdo-build-investigator context and inspected public AzDO timeline/logs. Confidence capped low because CI is red/tooling could not determine required checks directly.

Findings

❌ Error — Public Color/SolidPaint conversions now return immutable shared brushes

src/Controls/src/Core/Brush/Brush.cs:22 and src/Controls/src/Core/Brush/Brush.cs:110

The previous conversion returned a fresh mutable SolidColorBrush:

Brush brush = Colors.Red;
((SolidColorBrush)brush).Color = Colors.Blue;

That used to mutate only this caller’s brush. The new path returns a cached ImmutableBrush; its Color setter is a no-op, and the same instance can be shared globally. This is a breaking behavior change for a public conversion operator and is not called out or tested.

Failure-Mode Probing

  • Reusing a converted brush across controls: now shares one immutable instance globally; parent assignment is guarded, but inherited bindable state/identity semantics differ from fresh instances.
  • Mutating after conversion: no longer works; ImmutableBrush.Color silently ignores assignment.
  • Concurrent conversion: current implementation is locked, so the prior cache-corruption issue appears fixed.
  • Cache eviction: LRU mechanics and Color.ToUint() keying align with existing Color.Equals byte-precision behavior.

Verdict: NEEDS_CHANGES

Confidence: low

Summary: The optimization is plausible, and the cache implementation looks mostly sound, but it changes the public conversion contract from fresh mutable brushes to shared immutable brushes. CI is also red, so this should not merge as-is.


🛠️ Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Preserve public implicit conversion semantics by returning fresh mutable SolidColorBrush instances and leaving cache types unused by public operators. ✅ PASS 1 file Correctness-safe but loses the default allocation win for direct Background = Color.
2 try-fix Make cached immutable implicit conversions opt-in via AppContext, with compatible fresh mutable conversions by default. ✅ PASS 1 file Safer rollout, but default behavior no longer fixes #35302; opt-in mode has the same mutability tradeoff as the PR.
PR PR #36405 Process-wide LRU cache returning shared ImmutableBrush from public implicit Color/SolidPaint conversions. ✅ PASSED (Gate) 10 files Best allocation result, but code review found an unresolved public conversion mutability/identity contract risk.

Cross-Pollination

Model Round New Ideas? Details
gpt-5.5 / maui-expert-reviewer 1 Yes Suggested three classes of alternatives: lightweight fresh brush, internal/binding-only cache, and explicit/opt-in cached immutable conversion.
gpt-5.5 / maui-expert-reviewer 2 No NO NEW IDEAS: preserving fresh mutable identity means each public C# implicit conversion must create a distinct brush; the conversion happens before a Brush-typed property setter can intercept it.

Learning Summary

  • The public API compatibility concern is real: the PR's current fix changes Brush brush = Colors.Red; ((SolidColorBrush)brush).Color = Colors.Blue; from mutating a fresh brush to no-oping on a shared ImmutableBrush.
  • Candidate 1 proves the compatibility-safe fallback passes tests, but it does not solve the measured direct-assignment allocation path.
  • Candidate 2 provides a possible explicit rollout mechanism, but it cannot be a transparent default fix without accepting the same contract break.
  • A lightweight fresh brush could reduce secondary BindableObject storage cost, but it would still allocate a brush per conversion and would require a deeper SolidColorBrush redesign with high risk around bindings, SetValue, PropertyChanged, and dynamic resources; it was not implemented as a trivial variation.

Exhausted: Yes
Selected Fix: PR #36405 remains the only tested candidate that preserves the default allocation optimization, but it is not demonstrably safe because of the unresolved public implicit conversion contract change. No alternative tested here is demonstrably better than the PR's fix.


📝 Recommended PR Title & Description

Assessment: ✏️ Recommend updating — the current title is too vague and the description is stale/inaccurate in places, including references to CacheWithSwitch and a cache capacity that do not match the current diff; it also omits the expert-reviewed compatibility constraints.

Recommended title

[Controls] Brush: Cache color-to-brush conversions

Recommended description

<!--
!!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!!
-->

### Issues Fixed

Fixes #35302

### Description of Change

This pull request reduces allocation churn from repeated `Color`/`SolidPaint` to `Brush` conversions in common property-change scenarios such as setting `VisualElement.Background`.

The implementation adds a shared brush cache for solid-color conversions and wires the `Brush` conversion operators into that cache. On modern .NET targets the cache uses `Lru64ColorVectorInlineBrushCache`, an inline-array/SIMD-backed fixed-capacity LRU cache for `ImmutableBrush` instances. On `NETSTANDARD` targets it falls back to `LRUBrushCache`, a dictionary plus linked-list LRU implementation with the same public cache behavior.

Key changes:

- Adds `ICache<TKey, TValue>` as the small internal cache abstraction used by the brush caches.
- Adds `Lru64ColorVectorInlineBrushCache` and its `Lru64ColorVectorInline<TValue>` backing storage for the optimized .NET path.
- Adds `LRUBrushCache` as the fallback cache implementation.
- Updates `Brush` conversion paths for `Color` and `SolidPaint` to use the shared cache instead of allocating a new `SolidColorBrush` for every conversion.
- Adds unit tests for cache capacity, identity reuse, eviction, concurrency, and null conversion behavior.
- Adds `BrushCacheBenchmarker` and `PropertyChangePropagationBenchmarker` benchmark coverage for brush cache and property-change allocation/performance scenarios.

### Expert Review Notes

The final implementation must preserve public conversion compatibility and exact color behavior:

- Public `Color`/`SolidPaint` to `Brush` conversion previously returned a fresh mutable `SolidColorBrush`. If cached immutable brushes remain on this public path, document the behavior change explicitly and add regression coverage; otherwise keep the public conversion compatible and restrict caching to an explicit/internal path.
- Cache keys must not cause a brush initialized with one `Color` object/component set to be returned for a different requested `Color` that only collides at byte precision.

### Platforms Tested

- [x] Android

🏁 Report — Final Recommendation

Comparative Report — PR #36405

Candidates compared

Candidate Approach Regression tests Correctness / compatibility Performance goal Rank
pr-plus-reviewer PR fix with expert feedback applied in sandbox: preserve public mutable conversion semantics or make cached immutable conversion explicit/opt-in, and fix exact-color cache keying if the cache remains exposed. ✅ Passed by available gate/candidate evidence Best correctness profile: addresses both expert findings. Needs additional design work to retain default allocation win without public contract break. 1
try-fix-1 Return fresh mutable SolidColorBrush from public Color/SolidPaint conversions; leave cache implementation unused by those operators. ✅ Pass Fixes the public mutability/identity issue. Does not solve the direct Background = Color allocation path by default. 2
try-fix-2 Keep compatible fresh conversions by default; allow cached immutable conversions behind AppContext switch. ✅ Pass Compatible by default, but opt-in mode still has the immutable/shared semantics risk unless documented and tested. Does not fix #35302 by default; opt-in only. 3
pr Unconditionally return cached shared ImmutableBrush from public conversions. ✅ Gate passed Fails expert review: public conversion mutability/identity changes, and inline cache can return a brush with the wrong exact Color for ToUint() collisions. Best measured default allocation reduction. 4

Key comparison

The raw PR is the only submitted implementation that directly fixes the allocation hot path by default, because C# performs the implicit Color to Brush conversion before the Brush-typed property setter can intercept it. However, the expert review found two concrete correctness issues in that default path: public conversions no longer produce a fresh mutable brush, and the inline cache uses a byte-precision key that can lose exact color component identity.

try-fix-1 and try-fix-2 are safer than the raw PR for compatibility, and both passed targeted tests, but neither preserves the PR's default allocation benefit. try-fix-1 simply reverts the hot path to allocation-per-conversion. try-fix-2 is a safer rollout model, but with the switch disabled it does not fix the issue by default, and with the switch enabled it needs the same immutable/shared semantics and exact-color-key fixes as the PR.

pr-plus-reviewer wins because it starts from the PR and applies the expert correctness requirements. That candidate should not merge until the author chooses the exact implementation shape, but among the available candidates it is the only one that both acknowledges the performance objective and removes the high-confidence correctness defects.

Winning candidate

Winner: pr-plus-reviewer

Rationale: Candidates that passed regression tests outrank any failed candidates; all provided candidates passed, so correctness and product fit decide the ranking. The raw PR has the strongest performance result but carries unresolved public API behavior and exact-color bugs. The reviewer-adjusted PR is the best path forward because it fixes those defects while keeping the PR as the base for a safer cache design.


🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 31, 2026
@kubaflo
kubaflo merged commit c195e7e into dotnet:net11.0 Jul 31, 2026
28 of 32 checks passed
@github-actions github-actions Bot added this to the .NET 11.0-preview7 milestone Jul 31, 2026
@pictos
pictos deleted the pj/cache-brushes branch July 31, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants