[Impeller] Severe rasterizer regression (`CreateGlyphAtlas` pathology) under high distinct dynamic font workload (~600+ fonts)
- Dominant language
- Dart
- Stars
- 179k
- Forks
- 31.1k
- PR merge metrics
- PR metrics pending
Description
### Steps to reproduce
1. Create a Flutter application that loads a large set of unique dynamic font files (500+ distinct `.ttf`/`.woff2` fonts loaded dynamically per page/view).
2. Render dense, highly diacritic text (e.g., complex Arabic scripts with full tashkeel).
3. Swipe through views rapidly to force dynamic font switching and glyph rasterization.
4. Profile using DevTools / Observatory on an Android device comparing Impeller vs Skia (`--no-enable-impeller`).
### Expected results
Impeller should manage and cache glyph atlas textures efficiently across distinct dynamic font families without triggering expensive GPU thread blocking or dropped UI frames.
The rasterizer thread should process frames synchronously with the UI thread (locked ~1:1 ratio) with worst-case UI frames remaining under the 16.7 ms budget (60 FPS).
### Actual results
Impeller experiences a severe `CreateGlyphAtlas` bottleneck, blocking the GPU rasterization process for **45–57 ms (p50)** per frame transition, causing severe frame drops (jank) and pushing worst-case raster frames up to **143.6 ms**.
The UI thread becomes severely starved, resulting in a **151 : 598 (~1:4) Raster-to-UI frame ratio**, where the UI thread produces frames the rasterizer cannot consume in time.
Reverting to Skia (`--no-enable-impeller`) completely resolves the issue:
* `CreateGlyphAtlas` slices disappear entirely (0 ms).
* Median GPU raster cost drops by **12x** (61.0 ms → 5.0 ms).
* Worst-case GPU raster cost drops by **20x** (143.6 ms → 7.2 ms).
* Worst UI frame drops from 60.7 ms to **15.7 ms** (**Zero Jank**).
* Frame consumption locks back to **272 : 274 (~1:1)**.
#### Summary Table
| Metric | Impeller (Default) | Skia (`--no-enable-impeller`) | Impact / Variance |
| :--- | :--- | :--- | :--- |
| **`CreateGlyphAtlas` (Total/p50)** | **9,156 ms total** (p50: 45–57 ms) | **Absent entirely (0 ms)** | Skia bypasses atlas rebuild bottleneck |
| **`GPURasterizer::Draw` (p50)** | 61.0 ms | **5.0 ms** | **~12x speedup under Skia** |
| **`GPURasterizer::Draw` (worst)** | 143.6 ms | **7.2 ms** | **~20x improvement under Skia** |
| **Worst UI Frame** | 60.7 ms | **15.7 ms** | Below 16.7 ms budget (**Zero Jank**) |
| **Raster : UI Frame Ratio** | 151 : 598 (~1:4 bottleneck) | **272 : 274 (~1:1 locked)** | Impeller UI thread produces unconsumable frames |
### Code sample
``` dart
// Minimal conceptual reproduction for loading distinct dynamic fonts per page view:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const DynamicFontApp());
class DynamicFontApp extends StatelessWidget {
const DynamicFontApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: DynamicFontPageView(),
);
}
}
class DynamicFontPageView extends StatelessWidget {
const DynamicFontPageView({super.key});
// Simulates switching through views where each page requires a unique dynamic font family
Future _loadFontForPage(int pageIndex) async {
final fontLoader = FontLoader('CustomFont_$pageIndex');
// Load dynamic font asset bytes
final fontData = await rootBundle.load('assets/fonts/font_$pageIndex.ttf');
fontLoader.addFont(Future.value(fontData));
await fontLoader.load();
}
@override
Widget build(BuildContext context) {
return PageView.builder(
itemCount: 600,
itemBuilder: (context, index) {
return FutureBuilder(
future: _loadFontForPage(index),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const CircularProgressIndicator();
}
return Center(
child: Text(
'سُورَةٌ أَنزَلْنَٰهَا وَفَرَضْنَٰهَا وَأَنزَلْنَا فِيهَا ءَايَٰتٍ بَيِّنَٰتٍ لَّعَلَّكُمْ تَذَكَّرُونَ',
style: TextStyle(
fontFamily: 'CustomFont_$index',
fontSize: 24,
),
textAlign: TextAlign.center,
),
);
},
);
},
);
}
}
```
### Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
### Logs
=== Impeller Run Performance Trace ===
GPURasterizer::Draw p50: 61.0 ms
GPURasterizer::Draw worst: 143.6 ms
CreateGlyphAtlas: 9156 ms total, p50: 57.1 ms
worst UI frame: 60.7 ms
raster : UI frames: 151 : 598
=== Skia Run (--no-enable-impeller) Performance Trace ===
GPURasterizer::Draw p50: 5.0 ms
GPURasterizer::Draw worst: 7.2 ms
CreateGlyphAtlas: slice absent entirely
BulkUpdateAtlasBitmap: 4215 ms
worst UI frame: 15.7 ms
raster : UI frames: 284 : 283
### Flutter Doctor output
Doctor output
```console
~ % flutter doctor -v
[✓] Flutter (Channel stable, 3.44.8, on macOS 26.5.2 25F84 darwin-arm64, locale en-SA) [652ms]
• Flutter version 3.44.8 on channel stable at /Users//Documents/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 058e0af2c2 (2 days ago), 2026-07-23 10:56:21 -0700
• Engine revision 0cd610717b
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android,
enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file,
enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 37.0.0-rc1) [3.2s]
• Android SDK at /Users//Library/Android/sdk
• Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
• Platform android-CinnamonBun, build-tools 37.0.0-rc1
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.10+-117844308-b1163.108)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 26.6) [1,654ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17F113
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [6ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [10.1s]
• macOS (desktop) • macos • darwin-arm64 • macOS 26.5.2 25F84 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 150.0.7871.182
! Error: Browsing on the local area network for . Ensure the device is unlocked and attached with a
cable or associated with the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources [2.3s]
• All expected network resources are available.
```
Contributor guide
Research direction
Start by reproducing the dynamic-font workload on Android and capture DevTools or Observatory traces with Impeller and with --no-enable-impeller. Investigate the CreateGlyphAtlas and GPURasterizer::Draw paths using the reported frame timings as a baseline. Done means glyph-atlas work no longer causes the reported raster stalls and frame drops under the reproduction workload.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, dart
- Domain
- computer-graphics, mobile-dev, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100