Memory leak with Flutter Session Replay
Description
from: https://posthog.com/questions/memory-leak-with-flutter-session-replay
Asked questions on the link.
I have the same error message but when I have a TextFormField on the page and SessionReplay activated.
Once the focus in on the TextFormField I get that error message every seconds
flutter: Error: Failed to capture screenshot. flutter: Snapshot is the same as the last one.
My page also has SVG but removing them didn't change anything to the message - I don't know if it leads to a memory leak.
class OnboardingPage extends StatelessWidget {
const OnboardingPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Form(
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter your character name',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a name';
}
return null;
},
),
],
),
),
),
);
}
}
my config
final config =
PostHogConfig('xxx')
..debug = kDebugMode
..captureApplicationLifecycleEvents = true
..host = 'https://us.i.posthog.com'
..sessionReplay = true
..sessionReplayConfig.maskAllTexts = false
..sessionReplayConfig.maskAllImages = false;
A simple example of how I use it in my project using 4.9.1.
flutter: Snapshot is the same as the last one.
this is not an error btw, it's just logging.
I have the same error message but when I have a TextFormField on the page and SessionReplay activated.
Once the focus in on the TextFormField I get that error message every seconds
flutter: Error: Failed to capture screenshot. flutter: Snapshot is the same as the last one.
My page also has SVG but removing them didn't change anything to the message - I don't know if it leads to a memory leak.
class OnboardingPage extends StatelessWidget { const OnboardingPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Form( child: Column( children: [ TextFormField( decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Enter your character name', ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a name'; } return null; }, ), ], ), ), ), ); } }my config
final config = PostHogConfig('xxx') ..debug = kDebugMode ..captureApplicationLifecycleEvents = true ..host = 'https://us.i.posthog.com' ..sessionReplay = true ..sessionReplayConfig.maskAllTexts = false ..sessionReplayConfig.maskAllImages = false;A simple example of how I use it in my project using 4.9.1.
@JobiJoba this looks like a different issue, mind creating a new issue and providing an MRE? a sample where I can just run and reproduce the issue since I can't reproduce it myself? Thanks. I'll need to know which OS, version, Flutter version, etc.
Hello everyone, apologies for the delay. After further investigation, I’ve created a simple example that reliably reproduces the issue:
Main
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
import 'package:vize/core/config/analytics.dart';
import 'package:vize/core/config/env.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Env.load();
await AnalyticsConfig.configurePostHog();
runApp(
ChangeNotifierProvider<AppViewModel>(
create: (_) => AppViewModel(),
child: const App(),
),
);
}
class AppViewModel with ChangeNotifier {
AppViewModel() {
Timer.periodic(const Duration(milliseconds: 100), (_) {
notifyListeners();
});
}
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
SvgPicture.asset('assets/icons/profile/0.svg'),
Consumer<AppViewModel>(builder: (BuildContext context, AppViewModel viewModel, _) {
return SizedBox.shrink();
})
]);
}
}
PostHog Configuration
import 'package:posthog_flutter/posthog_flutter.dart';
import 'package:vize/core/config/env.dart';
class AnalyticsConfig {
AnalyticsConfig._();
static Future<void> configurePostHog() async {
final PostHogConfig config = PostHogConfig(Env.posthogApiKey)
..debug = false
..captureApplicationLifecycleEvents = false
..host = 'https://us.i.posthog.com'
..sessionReplay = true
..sessionReplayConfig.maskAllTexts = false
..sessionReplayConfig.maskAllImages = false
..sessionReplayConfig.throttleDelay = const Duration(milliseconds: 500);
await Posthog().setup(config);
}
}
- When sessionReplayConfig.throttleDelay is lowered, the memory leak accelerates.
- Increasing the Timer.periodic duration (e.g., 10 second instead of 100ms) eliminates the leak.
- Disabling SessionReplay eliminates the leak.
- The memory leak worsens as more SVGs are added to the page.
Tested on iOS 18.1
flutter version: 3.27.3 posthog_flutter version: 4.10.2 and olders provider version: 6.1.2
Let me know if further details or adjustments to the MRE are needed, and thanks for your help in investigating this!
@waskalien, which tools are you using to check for memory increase? standard memory view? Can you provide an MRE repo where I can just run the project? It should include your full MRE with size, provider, flutter_svg, etc. Since my sample with a simple SVG isn't enough, thanks.
Hi @marandaneto, I believe the issue does not appear in Flutter DevTools, but it is clearly visible in Xcode’s Memory tab. Here is the requested repository containing the MRE : https://github.com/waskalien/posthog-session-replay-memory-leak Thanks for looking into this!
@waskalien https://github.com/waskalien/posthog-session-replay-memory-leak/blob/68b7b0a7d3abb39634cd3c3d378a066a01491f54/lib/main.dart#L39-L41 is this something realistic? Session replay will only take a screenshot (or consume memory) if there are screen changes; apparently, you are forcing that. If you remove that, do you still see an issue? Is there a memory leak, or is it just consuming more memory? They are different things, though.
@marandaneto Yes, this is realistic in our case, as our app includes various real-time animations and a specific animation that requires a periodic timer running at 60 FPS.
As I mentioned before, if we remove the periodic timer, the memory leak disappears, which is the core issue here. This is not just higher memory consumption, it’s a true memory leak.
To illustrate: even if we do nothing and just display a static SVG with no movement, the app’s memory usage keeps increasing indefinitely. Wouldn’t you consider that a memory leak?
To illustrate: even if we do nothing and just display a static SVG with no movement, the app’s memory usage keeps increasing indefinitely. Wouldn’t you consider that a memory leak?
Maybe something is not correctly disposed, indeed. (I will take a look) Last question: If you stop the timer after a few seconds (when the memory is high), does the memory go down automatically or does it stay high up?
@marandaneto Thanks for looking into it!
After stopping the timer, some of the allocated memory is freed, but not all of it. For example, in a 50-second test, starting at 113 MB, memory increases to 125 MB. Once the timer is stopped, it drops to 119 MB, leaving a 6 MB residual increase that isn’t released.
During the timer’s execution, memory usage spikes even higher, and we can observe periodic small memory releases every few seconds. It forms a stair-step pattern, where some memory is freed, but not entirely. Let me know if you need more details
Improvements such as https://github.com/PostHog/posthog-flutter/issues/165 could help here, improving performance overall and not only the leak, I mean.
I found something here https://github.com/PostHog/posthog-flutter/pull/166 but it didnt improve much so its not the cause
So I finally narrowed down the issue to this line.
The problem isn't really memory leaking but a similar side effect.
We need an image so we have to call renderObject.toImage(...), and when we don't need it anymore, we call image.dispose() correctly, pretty much rather away.
The problem is that your screen is updating way too often, so we have to generate way too many images and the Dart/Flutter GC isn't running as often as it should, so a lot of images are in memory, because calling dispose is just a signal to be released, but only the Dart GC has the ability to free everything.
There's no way to call the GC manually sadly.
I think the best approach here is for you to increase your throttleDelay as much as possible or disable recordings for some specific screens that have constant animations. This would be possible with this feat request, but it's not done yet.
I understand the GC behavior, but I still believe this is an issue.
I’ve pushed a commit adding a button to cancel the timer that is updating the UI. You can test this by:
- Canceling the timer right after launch and checking memory.
- Canceling it after several minutes and comparing.
Even with no UI updates, memory remains higher, indicating a leak. As mentioned, Flutter DevTools doesn’t detect it, but Xcode does.
Cancelling timer right after launch :
Cancelling timer after 2min30sec :
The longer you wait before canceling the timer, the more significant the memory leak will be
@waskalien thanks for that.
I understand your point but I just don't have control over the GC, I cannot release the image after using it, we call dispose right away and that's the only thing we can do.
Do you have any other ideas?
@marandaneto I think it's because the image is passed to _getImageBytes, a new handle is created. All handles need to be disposed of before GC can it up. Have you tried debugGetOpenHandleStackTraces?
nope, debugGetOpenHandleStackTraces is new to me, will check today
The only thing I see is:
#0 new Image..
(dart:ui/painting.dart:1932:32) #1 new Image. (dart:ui/painting.dart:1934:6) #2 _NativeScene.toImage.. (dart:ui/compositing.dart:72:26)
and once I call .dispose, debugGetOpenHandleStackTraces always returns empty
Okay, then I was probably wrong. I thought that since the image was passed to _getImageBytes, a new handle would be created within _getImageBytes.
actually, we had the memory leak issue as well, but disabling impeller worked like a charm.