flame-engine / flame-engine/flame

Leaks detected through leak_tracker

Open
#3,974 6 comments 1 reaction 0 assignees View on GitHub
bug
Dominant language
Dart
Stars
10.8k
Forks
1k
Avg merge
1d 20h
Merged PRs (30d)
21

Description

### What happened?

I just tried enabling `package:leak_tracker` on my Flame game and it seems that Flame is leaking memory, or at the very least not properly disposing of objects.

From what I can tell, for example, `Component` is never disposed. (If I remember correctly from a discussion in a different forum, `onRemove` could be the end of the line, or it could be just before a component is re-parented. So that's not an equivalent of `dispose()`.)

But `PositionComponent` creates `_size`, which is a `NotifyingVector2`, which is a `ChangeNotifier`. The `dispose()` method is never called on it.

Similarly, `Transform2D` has `_offset` (a `NotifyingVector2`) that is also never disposed. In fact, `Transform2D` doesn't even have `dispose()` implemented, despite being a `ChangeNotifier` and owning at least 3 other `ChangeNotifier`s.

Now, these are all reported as `notDisposed` (more on that here https://github.com/dart-lang/leak_tracker/blob/main/doc/leak_tracking/CONCEPTS.md and https://github.com/dart-lang/leak_tracker/blob/main/doc/leak_tracking/TROUBLESHOOT.md). If there's never anyone listening (even indirectly) to the undisposed objects, then Dart GC will do its job. At least that's how I understand it. But it would still be cleaner if there were no leaks, not even _theoretical_ ones.

### What do you expect?

No leaks.

### How can we reproduce this?

The best method for me has been this. Let's say you have a sample or demo or a bigger game.

```yaml
dependencies:
leak_tracker: ^11.0.2
```

Then in main.dart:

```dart
if (!kReleaseMode && kLeakTrackingEnabled) {
log.fine('Setting up pkg:leak_tracker');
setupLeakTracking();
}
```

Then in something like `lib/utils/leak_tracking.dart`:

```dart
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';
import 'package:logging/logging.dart';

const bool kLeakTrackingEnabled = bool.fromEnvironment(
'track-leaks',
defaultValue: true,
);

const bool kLeakTrackingStackTraces = bool.fromEnvironment(
'track-leaks-stack-traces',
defaultValue: true,
);

/// Sets up leak tracking via `pkg:leak_tracker`.
void setupLeakTracking() {
final log = Logger('setupLeakTracking');

FlutterMemoryAllocations.instance.addListener(
(ObjectEvent event) => LeakTracking.dispatchObjectEvent(event.toMap()),
);
LeakTracking.phase = PhaseSettings(
leakDiagnosticConfig: LeakDiagnosticConfig(
collectStackTraceOnStart: kLeakTrackingStackTraces,
),
);
LeakTracking.start(config: LeakTrackingConfig(stdoutLeaks: false));

Timer.periodic(const Duration(seconds: 1), (_) async {
final leaks = await LeakTracking.collectLeaks();
if (leaks.total > 0) {
log.warning(
'${leaks.total} memory leaks detected by pkg:leak_tracker:\n'
'${leaks.toYaml(phasesAreTests: false)}',
);
if (!kLeakTrackingStackTraces) {
log.info(
'To learn where these objects were created, '
're-run the app with `--track-leaks-stack-traces true`.',
);
}
}
});
}

```

Then just run the sample and start going through it. Assuming you have logging set up properly, you'll be seeing things like:

```
40 memory leaks detected by pkg:leak_tracker:
# The text is generated by leak_tracker.
# For leak troubleshooting tips open:
# https://github.com/dart-lang/leak_tracker/blob/main/doc/leak_tracking/TROUBLESHOOT.md
notDisposed:
total: 40
objects:
NotifyingVector2:
identityHashCode: 424350735
context:
start: >
#6______setupLeakTracking._(package:giant_robot/util/leak_tracking.dart:22:41)
#7______FlutterMemoryAllocations.dispatchObjectEvent_(package:flutter/src/foundation/memory_allocations.dart:243:23)
#8______FlutterMemoryAllocations.dispatchObjectCreated_(package:flutter/src/foundation/memory_allocations.dart:281:5)
#9______debugMaybeDispatchCreated_(package:flutter/src/foundation/debug.dart:152:39)
#10_____ChangeNotifier.maybeDispatchObjectCreation._(package:flutter/src/foundation/change_notifier.dart:238:9)
#11_____ChangeNotifier.maybeDispatchObjectCreation_(package:flutter/src/foundation/change_notifier.dart:242:6)
#12_____ChangeNotifier.addListener_(package:flutter/src/foundation/change_notifier.dart:276:7)
#13_____new_PositionComponent_(package:flame/src/components/position_component.dart:96:11)
#14_____new__Entity&PositionComponent&HasGameReference_(package:giant_robot/game/entity/entity.dart)
#15_____new__Entity&PositionComponent&HasGameReference&CollisionCallbacks_(package:giant_robot/game/entity/entity.dart)
#16_____new__Entity&PositionComponent&HasGameReference&CollisionCallbacks&UpdateSeldom_(package:giant_robot/game/entity/entity.dart)
#17_____new_Entity_(package:giant_robot/game/entity/entity.dart:369:39)
#18_____WeaponPart._getFallbackProjectile_(package:giant_robot/game/combat/weapon_part.dart:677:12)
#19_____new_WeaponPart_(package:giant_robot/game/combat/weapon_part.dart:137:33)
#20_____createGblin_(package:giant_robot/game/entity/prototypes/gblin.dart:55:7)
#21______loadBadGuysFrom_(package:giant_robot/game/levels/level_definition_loader.dart:321:35)
...
```

The way you use the stacktrace is this:

1. Find the first fame that isn't in `setupLeakTracking` nor in a Flutter internal file. In the first case above, that's frame `#13`, which is `new PositionComponent`.
2. This tells you where the leaked object was _created_ (but never disposed).

In this particular case, `position_component.dart:96:11` is this line:

```dart
_size.addListener(_onModifiedSizeOrAnchor);
```

As explained in the TROUBLESHOOTING guide (linked above), ChangeNotifiers are registered only when someone first listens to them. There's a way to fix this and let a custom ChangeNotifier be registered at creation time. [Here](https://github.com/dart-lang/leak_tracker/blob/main/doc/leak_tracking/TROUBLESHOOT.md#more-complicated-cases). I think it would be good to have that set up for `NotifyingVector2` and `Transform2D`, at the very least.

### What steps should take to fix this?

_No response_

### Do have an example of where the bug occurs?

_No response_

### Relevant log output

```shell

```

### Execute in a terminal and put output into the code block below

Output of: flutter doctor -v

```
[✓] Flutter (Channel stable, 3.44.8, on macOS 15.7.7 24G720 darwin-arm64, locale en-US) [449ms]
• Flutter version 3.44.8 on channel stable at /Users/filiph/fvm/versions/stable
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 058e0af2c2 (9 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 36.0.0) [1,644ms]
• Android SDK at /Users/filiph/Library/Android/sdk
• Emulator version 36.4.9.0 (build_id 14788078) (CL:N/A)
• Platform android-36, build-tools 36.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 19+36-2238)
• All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 26.3) [947ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17C529
• CocoaPods version 1.16.2

[✓] Chrome - develop for the web [4ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Connected device (2 available) [6.4s]
• macOS (desktop) • macos • darwin-arm64 • macOS 15.7.7 24G720 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 150.0.7871.187
! Error: Browsing on the local area network for Filip Hracek’s iPad. 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)
! Error: Browsing on the local area network for Raindead S M4 iPad. 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 [539ms]
• All expected network resources are available.

• No issues found!

```

### Affected platforms

All

### Other information

I may be able to work on this but first, the Flame team needs to figure out whether `Components` should be disposed at some point, for example. If not, than much of this problem is unsolvable, as far as I can tell.

### Are you interested in working on a PR for this?

- [x] I want to work on this

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the report with package:leak_tracker and inspect position_component.dart, NotifyingVector2, and Transform2D, especially the _size listener and owned ChangeNotifiers. Clarify the Component disposal lifecycle with the Flame team, then define done as eliminating the reported undisposed objects or documenting which lifecycle cases are intentionally not disposable.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
game-dev, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.