Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,6 @@ src/Browser/Avalonia.Browser/wwwroot
api/diff
src/Browser/Avalonia.Browser/staticwebassets
.serena

# Claude agent worktrees
.claude/worktrees/
62 changes: 42 additions & 20 deletions src/Avalonia.Base/Media/FontManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,36 +351,58 @@ internal IReadOnlyList<Typeface> GetFamilyTypefaces(FontFamily fontFamily)
return [];
}

private bool TryGetFontCollection(Uri source, [NotNullWhen(true)] out IFontCollection? fontCollection)
internal bool TryGetFontCollection(Uri source, [NotNullWhen(true)] out IFontCollection? fontCollection)
{
Debug.Assert(source.IsAbsoluteUri);

if (source.Scheme == SystemFontScheme)
// Both the systemfont: scheme and SystemFontsKey (fonts:SystemFonts) map to the system
// font collection. SystemFontsKey is checked before the generic IsFontCollection branch
// so that the SystemFontCollection is created on demand regardless of which URI form is used.
if (source.Scheme == SystemFontScheme || source == SystemFontsKey)
{
source = SystemFontsKey;
fontCollection = GetOrCreateFontCollection(SystemFontsKey, PlatformImpl,
static (_, impl) => new SystemFontCollection(impl));
return true;
}
Comment thread
Gillibald marked this conversation as resolved.

if (!_fontCollections.TryGetValue(source, out fontCollection))
// Other fonts: URIs are only returned when they have been explicitly registered
// via AddFontCollection — no implicit creation to avoid caching null for unknown keys.
if (source.IsFontCollection())
{
if (source == SystemFontsKey)
{
fontCollection = new SystemFontCollection(PlatformImpl);
}
else
{
if (source.IsAbsoluteResm() || source.IsAvares())
{
fontCollection = new EmbeddedFontCollection(source, source);
}
}
return _fontCollections.TryGetValue(source, out fontCollection);
}

if (fontCollection != null)
{
return _fontCollections.TryAdd(fontCollection.Key, fontCollection);
}
if (source.IsAbsoluteResm() || source.IsAvares())
{
fontCollection = GetOrCreateFontCollection(source, 0,
static (key, _) => new EmbeddedFontCollection(key, key));
return true;
}

return fontCollection != null;
fontCollection = null;
return false;
}

/// <summary>
/// Thread-safe get-or-create that disposes any candidate that loses the insertion race,
/// preventing resource leaks that <see cref="ConcurrentDictionary{TKey,TValue}.GetOrAdd(TKey,Func{TKey,TValue})"/>
/// can cause when the factory is invoked concurrently by multiple threads.
/// </summary>
private IFontCollection GetOrCreateFontCollection<TState>(Uri key, TState state, Func<Uri, TState, IFontCollection> factory)
{
if (_fontCollections.TryGetValue(key, out var existing))
return existing;

var candidate = factory(key, state);

// GetOrAdd(key, value) atomically inserts or returns the existing value;
// it never invokes a factory, so only one IFontCollection instance survives.
var winner = _fontCollections.GetOrAdd(key, candidate);

if (!ReferenceEquals(winner, candidate))
candidate.Dispose(); // Our candidate lost the race – dispose it to avoid the leak.

return winner;
}

private string GetDefaultFontFamilyName(FontManagerOptions? options)
Expand Down
51 changes: 51 additions & 0 deletions tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
Expand Down Expand Up @@ -86,5 +88,54 @@ public void Should_Return_First_Installed_Font_Family_Name_When_Default_Family_N
Assert.Equal("DejaVu", FontManager.Current.DefaultFontFamily.Name);
}
}

[Fact]
public async Task TryGetGlyphTypeface_Should_Be_Thread_Safe_For_Embedded_Fonts()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var fontManager = FontManager.Current;

const string fontUri =
"resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono";
var collectionKey =
new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests");

// Warm up to validate the font URI is correct.
Assert.True(fontManager.TryGetGlyphTypeface(new Typeface(new FontFamily(fontUri)), out _));

const int iterations = 50;
int failures = 0;

for (int i = 0; i < iterations; i++)
{
fontManager.RemoveFontCollection(collectionKey);

using var barrier = new Barrier(2);
bool r1 = false, r2 = false;

var t1 = Task.Run(() =>
{
barrier.SignalAndWait();
r1 = fontManager.TryGetGlyphTypeface(new Typeface(new FontFamily(fontUri)), out _);
}, TestContext.Current.CancellationToken);

var t2 = Task.Run(() =>
{
barrier.SignalAndWait();
r2 = fontManager.TryGetGlyphTypeface(new Typeface(new FontFamily(fontUri)), out _);
}, TestContext.Current.CancellationToken);

await Task.WhenAll(t1, t2);

if (!r1 || !r2)
{
Interlocked.Increment(ref failures);
}
}

Assert.Equal(0, failures);
}
}
}
}
Loading
Loading