Skip to content

[camera_android][0.10.10+2] Pausing the debugger (reaching a breakpoint) after startImageStream() causes memory leak, then OOM crash #166533

Open
flutter/packages
#8998
@kwikwag

Description

@kwikwag

Steps to reproduce

If the debugger is paused any time after startImageStream() is called, or if the main thread hangs for long enough for some other reason, there appears to be some kind of buffer that's filling up causing an out-of-memory error to ensue and the app crashes.

My use-case: I need to run the images from the camera through a machine-learning model (TFLite/LiteRT). For this purpose I use the startImageStream() listener.

I cannot use the endorsed CameraX implementation of camera 0.11.1 (camera_android_camerax) due to a show-stopping bug (#152763) that crashes the app spontaneously with a NullPointerException when using startImageStream().

This is distinct from #97941, which affected iOS.

Expected results

Using startImageStream() should not crash the app, neither spontaneously nor when the debugger is paused or the main thread otherwise busy.

Actual results

In camera 0.10.6, or camera 0.11.1 with camera_android 0.10.10+2, when pausing the debugger at some point where the image stream is being listened to (startImageStream() was called), the app accumulates memory and eventually crashes due to a platform OOM (out-of-memory) error. Equivalently, the crash will occur if the main thread hangs for long enough.

Code sample

Code sample

pubspec.yaml

name: issue_camera_stream
description: "Demonstration of issue in Flutter Camera plugin of memory leak when the main thread is halted after using startImageStream()."
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ^3.7.2

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

  camera: ^0.11.1
  # Using the endorsed Android implementation camera_android_camerax causes issue #152763
  camera_android: ^0.10.10+2

  # Version 0.10.6 has the same problem
  # camera: ^0.10.6

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

main.dart

import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';


class CameraManager {
  bool _isReady = false;
  CameraController? _cameraController;
  final _listeners = List<onLatestImageAvailable>.empty(growable: true); 

  bool get isReady => _isReady;

  CameraController get controller {
    final cameraController = _cameraController;
    if (!_isReady || cameraController == null) {
      throw Exception("Invalid camera manager state");
    }
    return cameraController;
  }

  void addListener(onLatestImageAvailable callback) {
    _listeners.add(callback);
  }
  
  bool removeListener(onLatestImageAvailable callback) {
    return _listeners.remove(callback);
  }

  onLatestImageAvailableHandler(CameraImage image) {
    final listeners = [..._listeners];
    for (var listener in listeners) {
      try {
        listener(image);
      }
      catch (e) {
        // ignore callback exceptions
      }
    }
  }

  CameraManager() {
    // Ensure that plugin services are initialized so that `availableCameras()`
    // can be called before `runApp()`
    WidgetsFlutterBinding.ensureInitialized();
    // Obtain a list of the available cameras on the device.
    availableCameras().then((cameras) async {
      final backCameraDescription = cameras.firstWhere((camera) => camera.lensDirection == CameraLensDirection.back);
      final cameraController = CameraController(backCameraDescription, ResolutionPreset.high);
      await cameraController.initialize();
      cameraController.startImageStream(onLatestImageAvailableHandler);
      _cameraController = cameraController;
      _isReady = true;
    });
  }

  dispose() {
    _isReady = false;
    final cameraController = _cameraController;
    if (cameraController == null) {
      return;
    }
    _cameraController = null;
    cameraController.dispose();
  }
}

late CameraManager _cameraManager;

void main() {
  _cameraManager = CameraManager();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // title: 'Flutter Camera startImageStream Issue',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  // const HomePage({super.key, required this.title});
  const HomePage({super.key});

  // final String title;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _counter = 0;

  void _onLatestImageAvailableHandler(CameraImage image) {
    setState(() {
      _counter++;
      if (_counter % 100 == 0) {
        sleep(Duration(seconds: 30));
      }
    });
  }

  @override
  void initState() {
    super.initState();
    _cameraManager.addListener(_onLatestImageAvailableHandler);
  }

  @override
  void dispose() {
    super.dispose();
    _cameraManager.removeListener(_onLatestImageAvailableHandler);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // appBar: AppBar(
      //   backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      //   title: Text(widget.title),
      // ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            _counter > 0? CameraPreview(_cameraManager.controller) :
              const Text('The camera is starting...'),
            if (_counter != 0) Text(
              ((_counter + 1) % 100 == 0) ? 'Sleeping for 30s...' : '$_counter frames',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () { setState(() {
          _counter = 0;
        }); },
        tooltip: 'Reset count',
        child: const Icon(Icons.restart_alt),
      ),
    );
  }
}

Screenshots or Video

Screenshots / Video demonstration

(The demo uses a slightly different version of the code, which doesn't halt every 100 frames, but instead I entered a debug breakpoint manually)

App with camera plugin 0.10.6 crashes within seconds of reaching the breakpoint. The same thing happens with camera 0.11.1 together with camera_android 0.10.10+2.

issue_flutter_camera-0.10.6-android_memory_leak.webm

Same app with camera plugin version 0.11.1 has no memory leak when reaching breakpoint, but crashes spontaneously after about one minute.

issue_flutter_camera-0.11.1-spontaneous_crash.webm

Logs

Logs
I/Camera  (16396): startPreview
I/ViewRootImpl@514ccc8[MainActivity](16396): handleWindowFocusChanged: 1 0 call from android.view.ViewRootImpl.-$$Nest$mhandleWindowFocusChanged:0
D/ViewRootImpl@514ccc8[MainActivity](16396): mThreadedRenderer.initializeIfNeeded()#2 mSurface={isValid=true 0xb400007a0f34fd50}
D/InputMethodManagerUtils(16396): startInputInner - Id : 0
I/InputMethodManager(16396): startInputInner - IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus
I/ViewRootImpl@514ccc8[MainActivity](16396): onDisplayChanged oldDisplayState=2 newDisplayState=2
I/CameraManagerGlobal(16396): Camera 0 facing CAMERA_FACING_BACK state now CAMERA_STATE_OPEN for client com.example.issue_camera_stream API Level 2 User Id 0
D/InputMethodManagerUtils(16396): startInputInner - Id : 0
I/InsetsController(16396): onStateChanged: host=com.example.issue_camera_stream/com.example.issue_camera_stream.MainActivity, from=android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl:7209, state=InsetsState: {mDisplayFrame=Rect(0, 0 - 1080, 2340), mDisplayCutout=DisplayCutout{insets=Rect(0, 80 - 0, 0) waterfall=Insets{left=0, top=0, right=0, bottom=0} boundingRect={Bounds=[Rect(0, 0 - 0, 0), Rect(511, 0 - 569, 80), Rect(0, 0 - 0, 0), Rect(0, 0 - 0, 0)]} cutoutPathParserInfo={CutoutPathParserInfo{displayWidth=1080 displayHeight=2340 physicalDisplayWidth=1080 physicalDisplayHeight=2340 density={2.8125} cutoutSpec={M 0,0 M 0,7.822222222222222‬‬ a 10.31111111111111‬,10.31111111111111‬ 0 1,0 0,20.62222222222222‬ a 10.31111111111111‬,10.31111111111111‬ 0 1,0 0,-20.62222222222222‬ Z @dp} rotation={0} scale={1.0} physicalPixelDisplaySizeRatio={1.0}}}}, mRoundedCorners=RoundedCorners{[RoundedCorner{position=TopLeft, radius=0, center=Point(0, 0)}, RoundedCorner{position=TopRight, radius=0, center=Point(0, 0)}, RoundedCorner{position=BottomRight, radius=0, center=Point(0, 0)}, RoundedCorner{position=BottomLeft, radius=0, center=Point(0, 0)}]}  mRoundedCornerFrame=Rect(0, 0 - 1080, 2340), mPrivacyIndicatorBounds=PrivacyIndicatorBounds {static bounds=Rect(956, 0 - 1080, 80) rotation=0}, mDisplayShape=DisplayShape{ spec=-311912193 displayWidth=1080 displayHeight=2340 physicalPixelDisplaySizeRatio=1.0 rotation=0 offsetX=0 offsetY=0 scale=1.0}, mSources= { InsetsSource: {d5340000 mType=statusBars mFrame=[0,0][1080,80] mVisible=true mFlags=[]}, InsetsSource: {d5340005 mType=mandatorySystemGestures mFrame=[0,0][1080,114] mVisible=true mFlags=[]}, InsetsSource: {d5340006 mType=tappableElement mFrame=[0,0][1080,80] mVisible=true mFlags=[]}, InsetsSource: {3 mType=ime mFrame=[0,0][0,0] mVisible=false mFlags=[]}, InsetsSource: {27 mType=displayCutout mFrame=[0,0][1080,80] mVisible=true mFlags=[]}, InsetsSource: {ea60001 mType=navigationBars mFrame=[0,2205][1080,2340] mVisible=true mFlags=[]}, InsetsSource: {ea60004 mType=systemGestures mFrame=[0,0][0,0] mVisible=true mFlags=[]}, InsetsSource: {ea60005 mType=mandatorySystemGestures mFrame=[0,2205][1080,2340] mVisible=true mFlags=[]}, InsetsSource: {ea60006 mType=tappableElement mFrame=[0,2205][1080,2340] mVisible=true mFlags=[]}, InsetsSource: {ea60024 mType=systemGestures mFrame=[0,0][0,0] mVisible=true mFlags=[]} }
I/InsetsSourceConsumer(16396): applyRequestedVisibilityToControl: visible=false, type=ime, host=com.example.issue_camera_stream/com.example.issue_camera_stream.MainActivity
I/Camera  (16396): CameraCaptureSession onConfigured
I/Camera  (16396): Updating builder settings
D/Camera  (16396): Updating builder with feature: ExposureLockFeature
D/Camera  (16396): Updating builder with feature: ExposurePointFeature
D/Camera  (16396): Updating builder with feature: ZoomLevelFeature
D/Camera  (16396): Updating builder with feature: AutoFocusFeature
D/Camera  (16396): Updating builder with feature: NoiseReductionFeature
I/Camera  (16396): updateNoiseReduction | currentSetting: fast
D/Camera  (16396): Updating builder with feature: FocusPointFeature
D/Camera  (16396): Updating builder with feature: ResolutionFeature
D/Camera  (16396): Updating builder with feature: SensorOrientationFeature
D/Camera  (16396): Updating builder with feature: FlashFeature
D/Camera  (16396): Updating builder with feature: ExposureOffsetFeature
D/Camera  (16396): Updating builder with feature: FpsRangeFeature
I/Camera  (16396): refreshPreviewCaptureSession
I/CameraManagerGlobal(16396): Camera 0 facing CAMERA_FACING_BACK state now CAMERA_STATE_ACTIVE for client com.example.issue_camera_stream API Level 2 User Id 0
I/CameraManagerGlobal(16396): Camera 0 facing CAMERA_FACING_BACK state now CAMERA_STATE_IDLE for client com.example.issue_camera_stream API Level 2 User Id 0
I/Camera  (16396): startPreviewWithImageStream
W/e_camera_stream(16396): Long monitor contention with owner main (16396) at void android.hardware.camera2.impl.CameraDeviceImpl.waitUntilIdle()(CameraDeviceImpl.java:1420) waiters=0 in void android.hardware.camera2.impl.CameraDeviceImpl$4.run() for 1.048s
I/Camera  (16396): CameraCaptureSession onConfigured
I/Camera  (16396): Updating builder settings
D/Camera  (16396): Updating builder with feature: ExposureLockFeature
D/Camera  (16396): Updating builder with feature: ExposurePointFeature
D/Camera  (16396): Updating builder with feature: ZoomLevelFeature
D/Camera  (16396): Updating builder with feature: AutoFocusFeature
D/Camera  (16396): Updating builder with feature: NoiseReductionFeature
I/Camera  (16396): updateNoiseReduction | currentSetting: fast
D/Camera  (16396): Updating builder with feature: FocusPointFeature
D/Camera  (16396): Updating builder with feature: ResolutionFeature
D/Camera  (16396): Updating builder with feature: SensorOrientationFeature
D/Camera  (16396): Updating builder with feature: FlashFeature
D/Camera  (16396): Updating builder with feature: ExposureOffsetFeature
D/Camera  (16396): Updating builder with feature: FpsRangeFeature
I/Camera  (16396): refreshPreviewCaptureSession
I/ViewRootImpl@514ccc8[MainActivity](16396): onDisplayChanged oldDisplayState=2 newDisplayState=2
I/CameraManagerGlobal(16396): Camera 0 facing CAMERA_FACING_BACK state now CAMERA_STATE_ACTIVE for client com.example.issue_camera_stream API Level 2 User Id 0
I/Camera  (16396): CameraCaptureSession onClosed
W/System  (16396): A resource failed to call Surface.release. 
D/ProfileInstaller(16396): Installing profile for com.example.issue_camera_stream
I/InsetsSourceConsumer(16396): applyRequestedVisibilityToControl: visible=true, type=statusBars, host=com.example.issue_camera_stream/com.example.issue_camera_stream.MainActivity
I/e_camera_stream(16396): Background concurrent mark compact GC freed 220KB AllocSpace bytes, 13(17MB) LOS objects, 49% free, 11MB/23MB, paused 1.875ms,7.842ms total 38.226ms
I/e_camera_stream(16396): Background concurrent mark compact GC freed 192KB AllocSpace bytes, 15(21MB) LOS objects, 49% free, 10MB/20MB, paused 345us,6.137ms total 42.585ms
I/e_camera_stream(16396): Background concurrent mark compact GC freed 192KB AllocSpace bytes, 17(26MB) LOS objects, 55% free, 4981KB/10MB, paused 260us,5.214ms total 45.834ms
I/e_camera_stream(16396): Background concurrent mark compact GC freed 160KB AllocSpace bytes, 2(2708KB) LOS objects, 49% free, 10MB/20MB, paused 209us,5.748ms total 25.547ms
I/e_camera_stream(16396): Waiting for a blocking GC ProfileSaver
I/e_camera_stream(16396): WaitForGcToComplete blocked ProfileSaver on Background for 14.154ms
I/e_camera_stream(16396): Background concurrent mark compact GC freed 160KB AllocSpace bytes, 12(16MB) LOS objects, 66% free, 3141KB/9285KB, paused 26.761ms,11.164ms total 55.595ms
I/e_camera_stream(16396): Clamp target GC heap from 270MB to 256MB
I/e_camera_stream(16396): Clamp target GC heap from 279MB to 256MB
I/e_camera_stream(16396): Starting a blocking GC Alloc
I/e_camera_stream(16396): Clamp target GC heap from 279MB to 256MB
I/e_camera_stream(16396): Alloc concurrent mark compact GC freed 96KB AllocSpace bytes, 0(0B) LOS objects, 0% free, 255MB/256MB, paused 248us,1.818ms total 33.830ms
I/e_camera_stream(16396): Forcing collection of SoftReferences for 900KB allocation
I/e_camera_stream(16396): Clamp target GC heap from 279MB to 256MB
I/e_camera_stream(16396): Alloc concurrent mark compact GC freed 32KB AllocSpace bytes, 0(0B) LOS objects, 0% free, 255MB/256MB, paused 202us,3.073ms total 19.283ms
W/e_camera_stream(16396): Throwing OutOfMemoryError "Failed to allocate a 921612 byte allocation with 334768 free bytes and 326KB until OOM, target footprint 268435456, growth limit 268435456" (VmSize 22294828 kB)
I/e_camera_stream(16396): Starting a blocking GC Alloc
I/e_camera_stream(16396): Clamp target GC heap from 279MB to 256MB
I/e_camera_stream(16396): Alloc concurrent mark compact GC freed 96KB AllocSpace bytes, 0(0B) LOS objects, 0% free, 255MB/256MB, paused 166us,2.292ms total 17.247ms
I/e_camera_stream(16396): Forcing collection of SoftReferences for 900KB allocation
I/e_camera_stream(16396): Clamp target GC heap from 279MB to 256MB
I/e_camera_stream(16396): Alloc concurrent mark compact GC freed 32KB AllocSpace bytes, 0(0B) LOS objects, 0% free, 255MB/256MB, paused 170us,2.118ms total 15.072ms
W/e_camera_stream(16396): Throwing OutOfMemoryError "Failed to allocate a 921616 byte allocation with 367536 free bytes and 358KB until OOM, target footprint 268435456, growth limit 268435456" (VmSize 22294828 kB)
E/AndroidRuntime(16396): FATAL EXCEPTION: CameraBackground
E/AndroidRuntime(16396): Process: com.example.issue_camera_stream, PID: 16396
E/AndroidRuntime(16396): java.lang.OutOfMemoryError: Failed to allocate a 921616 byte allocation with 367536 free bytes and 358KB until OOM, target footprint 268435456, growth limit 268435456
E/AndroidRuntime(16396): 	at io.flutter.plugins.camera.media.ImageStreamReader.parsePlanesForYuvOrJpeg(ImageStreamReader.java:151)
E/AndroidRuntime(16396): 	at io.flutter.plugins.camera.media.ImageStreamReader.onImageAvailable(ImageStreamReader.java:105)
E/AndroidRuntime(16396): 	at io.flutter.plugins.camera.media.ImageStreamReader.lambda$subscribeListener$2$io-flutter-plugins-camera-media-ImageStreamReader(ImageStreamReader.java:210)
E/AndroidRuntime(16396): 	at io.flutter.plugins.camera.media.ImageStreamReader$$ExternalSyntheticLambda2.onImageAvailable(D8$$SyntheticClass:0)
E/AndroidRuntime(16396): 	at android.media.ImageReader$1.run(ImageReader.java:947)
E/AndroidRuntime(16396): 	at android.os.Handler.handleCallback(Handler.java:958)
E/AndroidRuntime(16396): 	at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(16396): 	at android.os.Looper.loopOnce(Looper.java:230)
E/AndroidRuntime(16396): 	at android.os.Looper.loop(Looper.java:319)
E/AndroidRuntime(16396): 	at android.os.HandlerThread.run(HandlerThread.java:67)
I/Process (16396): Sending signal. PID: 16396 SIG: 9
Lost connection to device.

Exited.

Flutter Doctor output

Doctor output
$ flutter doctor -v
[✓] Flutter (Channel stable, 3.29.2, on Ubuntu 24.04.2 LTS 6.11.0-21-generic, locale en_US.UTF-8) [135ms]
    • Flutter version 3.29.2 on channel stable at /home/user/Apps/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision c236373904 (3 weeks ago), 2025-03-13 16:17:06 -0400
    • Engine revision 18b71d647a
    • Dart version 3.7.2
    • DevTools version 2.42.3

[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [2.9s]
    • Android SDK at /home/user/Apps/Android/Sdk/
    • Platform android-35, build-tools 35.0.0
    • Java binary at: /usr/bin/java
      This JDK was found in the system PATH.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.6+7-Ubuntu-124.04.1)
    • All Android licenses accepted.

[✗] Chrome - develop for the web (Cannot find Chrome executable at google-chrome) [24ms]
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.

[✗] Linux toolchain - develop for Linux desktop [89ms]
    ✗ clang++ is required for Linux development.
      It is likely available from your distribution (e.g.: apt install clang), or can be downloaded from https://releases.llvm.org/
    ✗ CMake is required for Linux development.
      It is likely available from your distribution (e.g.: apt install cmake), or can be downloaded from https://cmake.org/download/
    ✗ ninja is required for Linux development.
      It is likely available from your distribution (e.g.: apt install ninja-build), or can be downloaded from
      https://github.com/ninja-build/ninja/releases
    ✗ pkg-config is required for Linux development.
      It is likely available from your distribution (e.g.: apt install pkg-config), or can be downloaded from
      https://www.freedesktop.org/wiki/Software/pkg-config/

[!] Android Studio (not installed) [19ms]
    • Android Studio not found; download from https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/to/linux-android-setup for detailed instructions).

[✓] VS Code (version 1.98.2) [19ms]
    • VS Code at /snap/code/current/usr/share/code
    • Flutter extension version 3.108.0

[✓] Connected device (2 available) [252ms]
    • SM A546E (mobile) • XXXXXXXXXXX • android-arm64 • Android 14 (API 34)
    • Linux (desktop)   • linux       • linux-x64     • Ubuntu 24.04.2 LTS 6.11.0-21-generic

[✓] Network resources [702ms]
    • All expected network resources are available.

! Doctor found issues in 3 categories.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: crashStack traces logged to the consolefound in release: 3.29Found to occur in 3.29found in release: 3.31Found to occur in 3.31has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: cameraThe camera pluginperf: memoryPerformance issues related to memoryplatform-androidAndroid applications specificallyteam-androidOwned by Android platform teamtriaged-androidTriaged by Android platform team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions