posthog-flutter icon indicating copy to clipboard operation
posthog-flutter copied to clipboard

Memory leak with Flutter Session Replay

Open ioannisj opened this issue 1 year ago • 20 comments

Description

from: https://posthog.com/questions/memory-leak-with-flutter-session-replay

ioannisj avatar Dec 24 '24 10:12 ioannisj

Asked questions on the link.

marandaneto avatar Jan 07 '25 12:01 marandaneto

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 avatar Jan 07 '25 23:01 JobiJoba

flutter: Snapshot is the same as the last one.

this is not an error btw, it's just logging.

marandaneto avatar Jan 08 '25 08:01 marandaneto

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.

marandaneto avatar Jan 08 '25 08:01 marandaneto

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 avatar Feb 08 '25 16:02 waskalien

@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.

marandaneto avatar Feb 14 '25 07:02 marandaneto

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 avatar Feb 24 '25 11:02 waskalien

@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 avatar Feb 24 '25 11:02 marandaneto

@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?

waskalien avatar Feb 24 '25 11:02 waskalien

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 avatar Feb 25 '25 08:02 marandaneto

@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

waskalien avatar Feb 25 '25 11:02 waskalien

Image

So using the memory view on Dart/Flutter, the memory is pretty stable.

marandaneto avatar Feb 28 '25 08:02 marandaneto

Improvements such as https://github.com/PostHog/posthog-flutter/issues/165 could help here, improving performance overall and not only the leak, I mean.

marandaneto avatar Feb 28 '25 09:02 marandaneto

I found something here https://github.com/PostHog/posthog-flutter/pull/166 but it didnt improve much so its not the cause

marandaneto avatar Feb 28 '25 09:02 marandaneto

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.

marandaneto avatar Feb 28 '25 11:02 marandaneto

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 : Image

Cancelling timer after 2min30sec : Image

The longer you wait before canceling the timer, the more significant the memory leak will be

waskalien avatar Mar 03 '25 14:03 waskalien

@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 avatar Mar 03 '25 15:03 marandaneto

@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?

Image

RCVZ avatar Mar 26 '25 14:03 RCVZ

nope, debugGetOpenHandleStackTraces is new to me, will check today

marandaneto avatar Mar 27 '25 07:03 marandaneto

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

marandaneto avatar Mar 28 '25 12:03 marandaneto

Okay, then I was probably wrong. I thought that since the image was passed to _getImageBytes, a new handle would be created within _getImageBytes.

RCVZ avatar Mar 31 '25 09:03 RCVZ

actually, we had the memory leak issue as well, but disabling impeller worked like a charm.

hwkim1127 avatar Jun 16 '25 08:06 hwkim1127