From 55b26ecdce7d043039af942fa6e33fc5c37e3818 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Mon, 17 Aug 2026 12:02:32 +0500 Subject: [PATCH 1/7] Add regression test for bounded font cache --- test/src/font/font_asset_resolver_test.dart | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/src/font/font_asset_resolver_test.dart b/test/src/font/font_asset_resolver_test.dart index 22cf73d..078dd5c 100644 --- a/test/src/font/font_asset_resolver_test.dart +++ b/test/src/font/font_asset_resolver_test.dart @@ -95,6 +95,41 @@ void main() { expect(bundle.loadCount('assets/icons.ttf'), 1); }); + test('parsed font assets are evicted by the bounded cache', () async { + const retainedFontCount = 32; + final font = TestFontBuilder.trueType(); + final bundle = TestAssetBundle.fonts( + manifest: >{ + for (var index = 0; index <= retainedFontCount; index++) + 'Family$index': ['assets/font_$index.ttf'], + }, + assets: { + for (var index = 0; index <= retainedFontCount; index++) + 'assets/font_$index.ttf': font, + }, + ); + final resolver = FontAssetResolver(bundle); + + for (var index = 0; index <= retainedFontCount; index++) { + await resolver.resolve( + IconData( + TestFontBuilder.quadraticCodePoint, + fontFamily: 'Family$index', + ), + ); + } + expect(bundle.loadCount('assets/font_0.ttf'), 1); + + await resolver.resolve( + const IconData( + TestFontBuilder.compositeCodePoint, + fontFamily: 'Family0', + ), + ); + + expect(bundle.loadCount('assets/font_0.ttf'), 2); + }); + test('unique glyph streams evict old decoded outlines', () async { const glyphCount = 300; final resolver = FontAssetResolver(aliasedFixtureBundle(glyphCount)); From 4cdd7c1ff2b2f35562fc52fcb352300e845d7ea2 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Mon, 17 Aug 2026 12:09:08 +0500 Subject: [PATCH 2/7] Format parsed font cache regression test --- test/src/font/font_asset_resolver_test.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/src/font/font_asset_resolver_test.dart b/test/src/font/font_asset_resolver_test.dart index 078dd5c..194289c 100644 --- a/test/src/font/font_asset_resolver_test.dart +++ b/test/src/font/font_asset_resolver_test.dart @@ -98,15 +98,16 @@ void main() { test('parsed font assets are evicted by the bounded cache', () async { const retainedFontCount = 32; final font = TestFontBuilder.trueType(); + final manifest = >{}; + final assets = {}; + for (var index = 0; index <= retainedFontCount; index++) { + final asset = 'assets/font_$index.ttf'; + manifest['Family$index'] = [asset]; + assets[asset] = font; + } final bundle = TestAssetBundle.fonts( - manifest: >{ - for (var index = 0; index <= retainedFontCount; index++) - 'Family$index': ['assets/font_$index.ttf'], - }, - assets: { - for (var index = 0; index <= retainedFontCount; index++) - 'assets/font_$index.ttf': font, - }, + manifest: manifest, + assets: assets, ); final resolver = FontAssetResolver(bundle); From 0dd6347ff82678d2891a98a218756af961342f41 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Mon, 17 Aug 2026 12:11:52 +0500 Subject: [PATCH 3/7] Print formatter diff in CI --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 068afb0..9b06f35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,9 @@ jobs: working-directory: example - name: Check formatting - run: dart format --output=none --set-exit-if-changed . + run: | + dart format . + git diff --exit-code - name: Analyze run: flutter analyze --fatal-infos --fatal-warnings From 4ed791a61693f9bb09a7e67598481de3533de757 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Mon, 17 Aug 2026 12:13:27 +0500 Subject: [PATCH 4/7] Apply Dart formatting to font cache test --- test/src/font/font_asset_resolver_test.dart | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/src/font/font_asset_resolver_test.dart b/test/src/font/font_asset_resolver_test.dart index 194289c..2bcf6af 100644 --- a/test/src/font/font_asset_resolver_test.dart +++ b/test/src/font/font_asset_resolver_test.dart @@ -105,10 +105,7 @@ void main() { manifest['Family$index'] = [asset]; assets[asset] = font; } - final bundle = TestAssetBundle.fonts( - manifest: manifest, - assets: assets, - ); + final bundle = TestAssetBundle.fonts(manifest: manifest, assets: assets); final resolver = FontAssetResolver(bundle); for (var index = 0; index <= retainedFontCount; index++) { @@ -122,10 +119,7 @@ void main() { expect(bundle.loadCount('assets/font_0.ttf'), 1); await resolver.resolve( - const IconData( - TestFontBuilder.compositeCodePoint, - fontFamily: 'Family0', - ), + const IconData(TestFontBuilder.compositeCodePoint, fontFamily: 'Family0'), ); expect(bundle.loadCount('assets/font_0.ttf'), 2); From bc14576f3a3986ff0affcbad1ab1133cca8a9655 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Mon, 17 Aug 2026 12:13:52 +0500 Subject: [PATCH 5/7] Restore strict formatting check --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b06f35..068afb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,9 +61,7 @@ jobs: working-directory: example - name: Check formatting - run: | - dart format . - git diff --exit-code + run: dart format --output=none --set-exit-if-changed . - name: Analyze run: flutter analyze --fatal-infos --fatal-warnings From aa4641de5710975cfe6348785ba2158486e2c4b6 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Mon, 17 Aug 2026 12:17:21 +0500 Subject: [PATCH 6/7] Use const icon fixtures in font cache regression test --- test/src/font/font_asset_resolver_test.dart | 44 +++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/test/src/font/font_asset_resolver_test.dart b/test/src/font/font_asset_resolver_test.dart index 2bcf6af..b0062a1 100644 --- a/test/src/font/font_asset_resolver_test.dart +++ b/test/src/font/font_asset_resolver_test.dart @@ -97,6 +97,41 @@ void main() { test('parsed font assets are evicted by the bounded cache', () async { const retainedFontCount = 32; + const icons = [ + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family0'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family1'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family2'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family3'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family4'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family5'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family6'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family7'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family8'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family9'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family10'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family11'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family12'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family13'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family14'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family15'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family16'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family17'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family18'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family19'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family20'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family21'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family22'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family23'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family24'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family25'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family26'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family27'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family28'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family29'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family30'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family31'), + IconData(TestFontBuilder.quadraticCodePoint, fontFamily: 'Family32'), + ]; final font = TestFontBuilder.trueType(); final manifest = >{}; final assets = {}; @@ -108,13 +143,8 @@ void main() { final bundle = TestAssetBundle.fonts(manifest: manifest, assets: assets); final resolver = FontAssetResolver(bundle); - for (var index = 0; index <= retainedFontCount; index++) { - await resolver.resolve( - IconData( - TestFontBuilder.quadraticCodePoint, - fontFamily: 'Family$index', - ), - ); + for (final icon in icons) { + await resolver.resolve(icon); } expect(bundle.loadCount('assets/font_0.ttf'), 1); From e554267d4e42941b51f11006e56969706b265438 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Mon, 17 Aug 2026 12:19:52 +0500 Subject: [PATCH 7/7] Bound parsed OpenType font caching --- lib/src/font/font_asset_resolver.dart | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/src/font/font_asset_resolver.dart b/lib/src/font/font_asset_resolver.dart index 1f1988f..7ea3c31 100644 --- a/lib/src/font/font_asset_resolver.dart +++ b/lib/src/font/font_asset_resolver.dart @@ -8,6 +8,8 @@ import 'binary_reader.dart'; import 'font_selection.dart'; import 'open_type_font.dart'; +const _maximumCachedFonts = 32; +const _maximumRetainedFontBytes = 32 << 20; const _maximumCachedGlyphs = 256; const _maximumRetainedGlyphBytes = 8 << 20; typedef _GlyphCacheKey = (IconData, String, MorphFontSelection); @@ -18,8 +20,11 @@ final class FontAssetResolver { final AssetBundle bundle; Future>? _manifest; - final Map> _fonts = - >{}; + final SizedLruCache> _fonts = + SizedLruCache>( + maximumSize: _maximumCachedFonts, + maximumSizeBytes: _maximumRetainedFontBytes, + ); final SizedLruCache<_GlyphCacheKey, Future> _glyphs = SizedLruCache<_GlyphCacheKey, Future>( maximumSize: _maximumCachedGlyphs, @@ -166,17 +171,17 @@ final class FontAssetResolver { } Future _loadFont(String assetKey) async { - var future = _fonts[assetKey]; + var future = _fonts.get(assetKey); if (future == null) { future = _readFont(assetKey); - _fonts[assetKey] = future; + _fonts.put(assetKey, future); } try { - return await future; + final font = await future; + _fonts.updateSizeIfSame(assetKey, future, font.bytes.lengthInBytes); + return font; } catch (_) { - if (identical(_fonts[assetKey], future)) { - _fonts.remove(assetKey); - } + _fonts.removeIfSame(assetKey, future); rethrow; } }