Skip to content

[Impeller] Make the render target cache configurable #190613

Closed

Description

@luke-willi

Use case

We maintain a custom Flutter embedder on a Linux-based embedded platform and have observed a significant increase in GPU memory consumption and rasterisation cost when using Impeller EGL compared to SKIA EGL.

Our application contains complex UI effects, including blur filters. During rapid scrolling, GPU memory usage can increase substantially. Investigation of the engine identified RenderTargetCache and its keep_alive_frame_count configuration as a major contributor to resource usage.

By locally modifying the engine and reducing keep_alive_frame_count from the default value of 4 to 1 or 0, we observed significant reductions in peak GPU memory consumption. In complex blur animation scenarios, lowering the value to 1 or 0 also resulted in approximately 80% lower rasterisation time on our platform.

Different platforms have different memory, GPU and rendering constraints, and the current hardcoded value does not allow embedders to tune this behaviour for their specific hardware characteristics.

We are not aware of any existing Flutter package that can provide this functionality, as the setting exists within the engine and is not currently exposed through a public configuration interface.

Proposal

Expose Impeller's keep_alive_frame_count as a configurable engine/embedding parameter rather than a hardcoded value.

This would allow platform embedders to tune render target cache retention based on the capabilities and constraints of their target hardware. For example, embedded and memory-constrained devices may prefer lower values to reduce GPU memory usage and rasterisation overhead, while more capable devices could continue using the current default.

Ideally this could be configured through embedder settings or a launch-time engine argument.

engine/src/flutter/impeller/entity/render_target_cache.h
explicit RenderTargetCache(std::shared_ptr<Allocator> allocator, uint32_t keep_alive_frame_count = 4);

Activity

  1. added
    e: impellerImpeller rendering backend issues and features requests
    on Aug 5, 2026
  2. loic-sharma commented on Aug 5, 2026

    @loic-sharma
    Member

    Our application contains complex UI effects, including blur filters. During rapid scrolling, GPU memory usage can increase substantially.

    @luke-willi Thanks for creating this issue! Before we expose new configuration options, we'd like to investigate if we make Impeller more memory efficient in this scenario. Could you provide a minimal app that reproduces this on the latest Flutter Android or Flutter Linux?

    To reproduce this issue on Flutter Android or Flutter Linux, these steps might be useful:

    1. Switch to the latest Flutter main channel:

      $ flutter channel main
      $ flutter upgrade
      
    2. If using Flutter Android: configure it to use Impeller's OpenGLES backend by modifying the app's android/app/src/main/AndroidManifest.xml file and adding the following content in the <application> tag:

      <meta-data
        android:name="io.flutter.embedding.android.ImpellerBackend"
        android:value="opengles" />
    3. Run the app in profile mode:

      $ flutter run --profile
      
    4. Verify you see this console line:

      Using the Impeller rendering backend (OpenGLES).
      
    5. If using Flutter Android: you can use Android Studio's profiler to check the app's memory usage over time.

  3. gaaclarke commented on Aug 10, 2026

    @gaaclarke
    Member

    I'm hesitant to expose this value, it's a tuning value for an internal heuristic on how we clear out memory. If we expose it, it will tie our hands about the heuristics we are using. If you are building for custom embedders, don't you have an opportunity to tweak it yourself inside the source code?

  4. added
    P2Important issues not at the top of the work list
    on Aug 10, 2026
  5. loic-sharma commented on Aug 11, 2026

    @loic-sharma
    Member

    I agree we'd prefer not to expose this config if possible. The preferred solution would be for us to add optimizations for this scenario, and we'll need a minimal repro for that.

    @gaaclarke We recommend against creating custom patches on the Flutter engine, where possible. This makes it harder to keep custom embedders up-to-date.

  6. gaaclarke commented on Aug 11, 2026

    @gaaclarke
    Member

    @gaaclarke We recommend against creating custom patches on the Flutter engine, where possible. This makes it harder to keep custom embedders up-to-date.

    In the case where you are just changing constants, it shouldn't be difficult at all. We could make that even easier by percolating build constants to a config file so someones build environment could easily change them. I'm not sure if that's worth the effort though.

  7. gaaclarke commented on Aug 11, 2026

    @gaaclarke
    Member

    I mean embedder's are building their own engine, they could easily insert a script with a regex that switches the constant.

  8. luke-willi commented on Aug 17, 2026

    @luke-willi
    Author

    /@loic-sharma To test this we have been using an extreme case, an app that cycles through six full-screen, differently-colored Gaussian-blurred "pill" pages.

    main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_driver/driver_extension.dart';
    import 'blur.dart';
    
    void main() {
      enableFlutterDriverExtension();
      runApp(const BenchmarkApp());
    }
    
    class BenchmarkApp extends StatelessWidget {
      const BenchmarkApp({super.key});
      static const appTitle = 'EntOS Blur Benchmark';
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: appTitle,
          home: const Scaffold(body: Blur()),
        );
      }
    }
    
    blur.dart
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'dart:io';
    import 'dart:ui';
    
    class Blur extends StatefulWidget {
      const Blur({super.key});
    
      @override
      State<Blur> createState() => _BlurState();
    }
    
    class _BlurState extends State<Blur> {
      final PageController _pageController = PageController();
      final FocusNode _focusNode = FocusNode();
      int _currentPage = 0;
      late double _sigma;
    
      @override
      void initState() {
        super.initState();
        final envSigma = Platform.environment['BLUR_SIGMA'];
        _sigma = envSigma != null ? double.tryParse(envSigma) ?? 30.0 : 30.0;
        _focusNode.requestFocus();
      }
    
      void _next() {
        if (_currentPage < 5) {
          setState(() => _currentPage++);
          _pageController.animateToPage(
            _currentPage,
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeInOut,
          );
        }
      }
    
      void _previous() {
        if (_currentPage > 0) {
          setState(() => _currentPage--);
          _pageController.animateToPage(
            _currentPage,
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeInOut,
          );
        }
      }
    
      // jscpd:ignore-start
      Widget _getMenuItem(Color color, String text) {
        return ClipRRect(
          borderRadius: BorderRadius.circular(1000),
          child: BackdropFilter(
            filter: ImageFilter.blur(sigmaX: _sigma, sigmaY: _sigma),
            child: Container(
              color: color.withValues(alpha: 0.35),
              child: Center(
                child: Text(
                  text,
                  key: ValueKey('page_label_$text'),
                  style: const TextStyle(color: Colors.white, fontSize: 24),
                ),
              ),
            ),
          ),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Focus(
          focusNode: _focusNode,
          onKeyEvent: (node, event) {
            if (event is KeyDownEvent &&
                event.logicalKey == LogicalKeyboardKey.arrowLeft) {
              _previous();
              return KeyEventResult.handled;
            }
            if (event is KeyDownEvent &&
                event.logicalKey == LogicalKeyboardKey.arrowRight) {
              _next();
              return KeyEventResult.handled;
            }
            return KeyEventResult.ignored;
          },
          child: Center(
            child: SizedBox(
              height: 1080,
              child: Stack(
                children: [
                  Positioned.fill(
                    child: CustomPaint(painter: _BlurBackgroundPainter()),
                  ),
                  PageView(
                    controller: _pageController,
                    onPageChanged: (index) => setState(() => _currentPage = index),
                    children: [
                      _getMenuItem(Colors.red, '1'),
                      _getMenuItem(Colors.green, '2'),
                      _getMenuItem(Colors.blue, '3'),
                      _getMenuItem(Colors.yellow, '4'),
                      _getMenuItem(Colors.purple, '5'),
                      _getMenuItem(Colors.orange, '6'),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      }
      // jscpd:ignore-end
    
      @override
      void dispose() {
        _pageController.dispose();
        _focusNode.dispose();
        super.dispose();
      }
    }
    
    // Up handling is done via FocusScope.onKeyEvent above.
    
    class _BlurBackgroundPainter extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        final cols = 14;
        final rows = 10;
        final cellW = size.width / cols;
        final cellH = size.height / rows;
    
        final rect = Offset.zero & size;
        final bgPaint = Paint()
          ..shader = const LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Color(0xFF0F2027), Color(0xFF203A43), Color(0xFF2C5364)],
          ).createShader(rect);
        canvas.drawRect(rect, bgPaint);
    
        for (var y = 0; y < rows; y++) {
          for (var x = 0; x < cols; x++) {
            final hue = (x / cols) * 360.0;
            final light = 0.45 + 0.25 * ((y % 2 == 0) ? 1 : 0);
            final hsla = HSLColor.fromAHSL(0.8, hue, 0.7, light);
            final paint = Paint()..color = hsla.toColor();
            final r = Rect.fromLTWH(x * cellW, y * cellH, cellW, cellH);
            canvas.drawRect(r, paint);
          }
        }
    
        final linePaint = Paint()
          ..color = Colors.white.withValues(alpha: 0.08)
          ..strokeWidth = 1.0;
        const spacing = 12.0;
        for (double d = -size.height; d < size.width; d += spacing) {
          canvas.drawLine(
            Offset(d, 0),
            Offset(d + size.height, size.height),
            linePaint,
          );
        }
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
    }
    

    I ran this test using flutter drive, and measured the rasterisation time and GPU memory on a physical Android device - Samsung Galaxy S24+ (SM-S926B). Results were as followed:
    Rasterizer time (ms)

    Metric keep_alive_frame_count=4 (default) keep_alive_frame_count=1 Change
    Average 26.97 20.17 −25.2%
    90th percentile 34.54 24.06 −30.3%
    99th percentile 72.98 26.46 −63.7%
    Worst frame 92.11 42.53 −53.8%

    GPU memory (dumpsys meminfo Graphics PSS, KB)

    Metric keep_alive_frame_count=4 (default) keep_alive_frame_count=1 Change
    Idle ~435,253 ~434,504 ~0%
    Post-drive (peak) 954,964 576,105 −39.7%
  9. gaaclarke commented on Sep 18, 2026

    @gaaclarke
    Member

    @luke-willi thanks for the reproduction. Some thoughts.

    The reproduction is executed on an android device. It's important to note the issue is now saying it's reproducible on multiple devices whereas the original description framed this as something unique to a particular hardware.

    The reproduction is showing us good movement in numbers, but it is still a contrived benchmark so we have to be careful about accommodating it too much. It is informed by your real world usage too though.

    I think there is good evidence here that there may be an improvement that can be made that benefits all (or most) flutter developers. We should explore that avenue. Having a hidden flag benefits few users. We really don't want to be in a position where people have to tweak hyper parameters to get optimal performance from Flutter.

    Can you clarify if keep_alive_frame_count=0 is your requirement? That is mentioned in the description but the repro shows benchmarks for keep_alive_frame_count=1.

    As a next step I'd like to see the benchmark results for standard Flutter usage with keep_alive_frame_count=1 ( example: https://flutter-flutter-perf.luci.app/e/?begin=1789666314&enable_chart_tooltip=true&end=1789752714&plotSummary=true&queries=sub_result%3Daverage_gpu_memory_mb%26sub_result%3Daverage_frame_rasterizer_time_millis%26test%3Dnew_gallery_impeller__transition_perf&use_titles=true )

    We might be able to just change that with proper evidence in-hand. I wasn't around for the selection of 4, so I'm not sure what went into that decision. We should do some git archeology there.

  10. added
    P1High-priority issues at the top of the work list
    and removed
    P2Important issues not at the top of the work list
    on Sep 18, 2026
  11. gaaclarke commented on Sep 18, 2026

    @gaaclarke
    Member

    P1 for investigation of standard performance benchmarks with keep_alive_frame_count=1 and archeology into keep_alive_frame_count=4.

  12. 11 remaining items

  13. changed the title [-]Configuration of Impeller EGL keep_alive_frame_count[/-] [+][Impeller] Make the render target cache configurable[/+] on Oct 1, 2026
  14. mk-towers commented on Oct 5, 2026

    @mk-towers

    @gaaclarke Thanks for investigating this.

    We performed some additional testing on Android across a couple of physical devices and the Android emulator. We are consistently seeing lower graphics memory usage with KAFC=0 and KAFC=1 versus KAFC=4 but, consistent with your results, we don't see a consistent impact on rasterizer time. Possibly the rasterizer time difference noted above is device-specific.

    I tested #193027 (cherry-picked to 3.47.5) on one of our embedded Linux devices and it works well:

    • As written, this has a positive impact on peak graphics memory usage.
    • With cache_budget_bytes reduced to its minimum value, graphics memory usage is similar to KAFC=0 on the test app.
    • With cache_budget_bytes reduced to its minimum value, our main App look OK (manual test in release mode) and graphics memory usage is significantly reduced.

    If #193027 is merged, are you planning to keep cache_budget_bytes as an internal value or expose it?

  15. gaaclarke commented on Oct 5, 2026

    @gaaclarke
    Member

    @mk-towers thanks for testing it out. What differences were you seeing in memory? My tests just showed a 5 MB different in the peak usage. I'm uncertain if that is a change worth accepting the extra complexity over.

    If #193027 is merged, are you planning to keep cache_budget_bytes as an internal value or expose it?

    I'm not going to preemptively expose flags without any evidence that it's required.

  16. mk-towers commented on Oct 7, 2026

    @mk-towers

    @gaaclarke I've done some additional testing and the results are very inconsistent. I think we need a better test case that's more consistent and representative of real-world usage. I'll work on one and share the results.

  17. github-actions commented on Oct 12, 2026

    @github-actions

    Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now.

    If you find this problem please file a new issue with the same description, what happens, logs and the output of 'flutter doctor -v'. All system setups can be slightly different so it's always better to open new issues and reference the related ones.

    Thanks for your contribution.

  18. loic-sharma commented on Oct 12, 2026

    @loic-sharma
    Member

    @mk-towers Our bot closed this issue because the original reporter, @luke-willi, hasn't replied in 3 weeks. Once you have a better test case, could you file a new issue? Please cc @gaaclarke and I on that new issue once it's ready :)

  19. github-actions commented on Oct 26, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  20. locked as resolved and limited conversation to collaborators on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority issues at the top of the work listcustomer: bluecustomer: goodlifee: impellerImpeller rendering backend issues and features requeststeam-engineOwned by Engine teamtriaged-engineTriaged by Engine teamwaiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions