Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memory leak when configuring amplify #5161

Closed
4 of 14 tasks
kamami opened this issue Jul 15, 2024 · 10 comments
Closed
4 of 14 tasks

Memory leak when configuring amplify #5161

kamami opened this issue Jul 15, 2024 · 10 comments
Assignees
Labels
core Issues related to the Amplify Core Plugin pending-close-response-required The issue will be closed if details necessary to reproduce the issue are not provided within 7 days. pending-community-response Pending response from the issue opener or other community members question A question about the Amplify Flutter libraries

Comments

@kamami
Copy link

kamami commented Jul 15, 2024

Description

I am configuring amplify in a function, which is called multiple times by native code while the app is running. It is designed to track the users location even in the background. I also want to access my API Gateway and Authentication Services there to send the GPS data to my AWS backend.

I am observing following:

When calling _configureAmplify I get a permanent increased memory usage on my iPhone 15 pro. When call _configureAmplify, but without the lines await Amplify.addPlugins([authPlugin, apiPlugin]); I do not get an increased memory usage. But this is not an option, because I need those plugins.

import 'dart:convert';
import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:codrive/repositories/api_repository.dart';
import 'package:codrive/repositories/auth_repository.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

Future<void> _configureAmplify() async {
  const String env = String.fromEnvironment('ENV');
  String configFile = 'assets/config/config.$env.json';

  try {
    String s = await rootBundle.loadString(configFile);
    Map<String, dynamic> config = jsonDecode(s);
    AmplifyAuthCognito authPlugin = AmplifyAuthCognito();
    AmplifyAPI apiPlugin = AmplifyAPI();
    await Amplify.addPlugins([authPlugin, apiPlugin]);

    await Amplify.configure(jsonEncode(config['amplifyConfig']));
  } on AmplifyAlreadyConfiguredException {
    print(
        'Tried to reconfigure Amplify; this can occur when your app restarts on Android.');
  } catch (e) {
    print('Failed to configure Amplify: $e');
  }
}

@pragma('vm:entry-point')
void backgroundCallbackDispatcher() async {
  WidgetsFlutterBinding.ensureInitialized();
  print(Amplify.isConfigured);
  await _configureAmplify();

  final apiRepository = APIRepository();
  final authRepository = AuthRepository();

   //track users location

}

Categories

  • Analytics
  • API (REST)
  • API (GraphQL)
  • Auth
  • Authenticator
  • DataStore
  • Notifications (Push)
  • Storage

Steps to Reproduce

Run _configureAmplify with and without await Amplify.addPlugins([authPlugin, apiPlugin]);

Screenshots

This is the chart, when adding both plugins before configuration. Every step is one invocation of backgroundCallbackDispatcher.

image

This is the chart, when NOT adding both plugins before configuration.

image

Platforms

  • iOS
  • Android
  • Web
  • macOS
  • Windows
  • Linux

Flutter Version

3.22.2

Amplify Flutter Version

2.2.0

Deployment Method

Amplify CLI

Schema

No response

@Equartey Equartey added core Issues related to the Amplify Core Plugin pending-triage This issue is in the backlog of issues to triage labels Jul 15, 2024
@Equartey
Copy link
Member

Hi @kamami,

Typically, we recommend using Amplify.isConfigured to ensure Amplify.configure only runs when the app is in an unconfigured state. Is something like this viable for you?

@pragma('vm:entry-point')
void backgroundCallbackDispatcher() async {
  WidgetsFlutterBinding.ensureInitialized();
  print(Amplify.isConfigured);
  // conditionally configure
  if (!Amplify.isConfigured) {
    await _configureAmplify();
  }
  final apiRepository = APIRepository();
  final authRepository = AuthRepository();

   //track users location

}

If not, can you further explain your use case for calling Amplify.configure multiple times?

@Equartey Equartey added the pending-community-response Pending response from the issue opener or other community members label Jul 15, 2024
@kamami
Copy link
Author

kamami commented Jul 15, 2024

Hi @Equartey ,
thanks for the quick response. I already tried Amplify.isConfigured, but it is always false.
The backgroundCallbackDispatcher is another entry-point for my app. This part of the app is meant to run in the background even if the app is detached. The location tracking is done by an SDK which uses iOS microtasks to invoke and run the backgroundCallbackDispatcher directly. Since iOS microtasks are killed by the OS after 20-30 secs, the backgroundCallbackDispatcher is invoked again, which triggers a new Amplify configuration. The configuration is not the problem, but adding the plugins before configuration causes the increased memory usage.

I think using Amplify in an additional entry point (@pragma('vm:entry-point')) is a valid use case, but that also means multiple configurations need to be handled correctly.

BTW. using the same approach with Firebase/Firestore is working perfectly fine. The multiple initializations of Firebase are not causing any problems.

@pragma('vm:entry-point')
void backgroundCallbackDispatcher() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  await FirebaseAuth.instance.signInAnonymously();
  const uuid = Uuid();

  SafeDrivePodSDK.podData.stream().listen((event) async {
    if (event is PodDateTimeRead) {
      // Wait 15 seconds to ensure the connection is stable
      if (Platform.isIOS) {
        await Future<void>.delayed(
          const Duration(seconds: 15),
        );
      }

      SafeDrivePodSDK.podData.downloadTrips(
        fromDate: DateTime.now().subtract(
          const Duration(days: 365 * 10),
        ),
      );
    }

    final userId = FirebaseAuth.instance.currentUser?.uid ?? 'anonymous';
    final date = DateTime.now();
    final day = DateFormat('yyyy-MM-dd').format(date);
    final key = '${DateFormat('HH:mm:ss:SS').format(date)}_[${uuid.v4()}]';
    final path = 'users/$userId/data/$day/events/$key';

    FirebaseFirestore.instance.doc(path).set(
          event.toMap(),
        );
  });
}

@Equartey
Copy link
Member

Thank you for the context, that's really helpful.

Couple comments and questions:

  • When you have multiple calls to Amplify.configure are you also getting multiple AmplifyAlreadyConfiguredExceptions?
  • Multiple calls to configure are not officially supported, but there is a feature request to address this.
  • There is an internal API (that is subject to change at anytime), Amplify.reset() that you can try to call before calling _configureAmplify(). This is more of a short term solution, but I would be curious to know if this changes the observed behavior.
  • What is the framework responsible for this annotation @pragma('vm:entry-point')?

@kamami
Copy link
Author

kamami commented Jul 15, 2024

Bildschirmfoto 2024-07-15 um 20 36 13

The red errors show the time when await Amplify.addPlugins([authPlugin, apiPlugin]); was invoked. I would not mind the increase of memory, but it never drops down again. Each configuration (only when adding plugins before) is increasing the level permanently.

@Equartey
Copy link
Member

Hi @kamami, we're working on reproducing this to validate some assumptions.

In the meantime, we suspect that each call to backgroundCallbackDispatcher spawns a new isolate without context of previous configure calls. This would explain why Amplify.isConfigured is returning false.

If this hypothesis is correct, calling Amplify.reset() after your business logic may help by cleaning up memory to avoid additional calls consuming more. Can you give that a try and report your findings?

@kamami
Copy link
Author

kamami commented Jul 15, 2024

The point when my business logic is done, is not always clear. The backgroundCallbackDispatcher is not only called again, when the app is killed. In this case I could observe the lifecycle of the app. Instead it is also called sometimes a second and third time, while the app is running. But this should not harm the performance as it unfortunately currently does. Here is my code for now. As you can see I reset Amplify before I do another configuration:

Future<void> _configureAmplify() async {
  try {
    await Amplify.reset();
    const String env = String.fromEnvironment('ENV');
    String configFile = 'assets/config/config.$env.json';
    String s = await rootBundle.loadString(configFile);
    Map<String, dynamic> config = jsonDecode(s);
    AmplifyAuthCognito authPlugin = AmplifyAuthCognito();
    AmplifyAPI apiPlugin = AmplifyAPI();
    await Amplify.addPlugins([authPlugin, apiPlugin]);

    await Amplify.configure(jsonEncode(config['amplifyConfig']));
  } on AmplifyAlreadyConfiguredException {
    print('Tried to reconfigure Amplify; this can occur when your app restarts on Android.');
  } catch (e) {
    print('Failed to configure Amplify: $e');
  }
}

@pragma('vm:entry-point')
void backgroundCallbackDispatcher() async {
  WidgetsFlutterBinding.ensureInitialized();
  await _configureAmplify();

  final apiRepository = APIRepository();
  final authRepository = AuthRepository();

  try {
    SafeDrivePodSDK.podData.stream().listen((PodDataEvent event) async {
      await _handlePodEvent(event, apiRepository, authRepository);
    }, onError: (error) {
      safePrint('Stream error: $error');
    }, onDone: () {
      safePrint('Stream closed');
    }, cancelOnError: true);
  } catch (e) {
    safePrint('Error in background callback dispatcher: $e');
  }
}

@Equartey Equartey added question A question about the Amplify Flutter libraries and removed pending-community-response Pending response from the issue opener or other community members pending-triage This issue is in the backlog of issues to triage labels Jul 16, 2024
@Equartey
Copy link
Member

@kamami I've been unsuccessful with my attempts to observe a consistent memory increase like the graph you shared. I've seen memory usage spike, but it returns to base line after a few seconds. Given the challenge of recreating this environment, offering more specific steps or a sample app could be beneficial.

Ultimately, until we have official support for making multiple calls to Amplify.configure(), potential workarounds are:

  • Ensure Amplify get's reset after each call to backgroundCallbackDispatcher, ie through app life cycle
  • Or find a way to share configuration state to each call of backgroundCallbackDispatcher

@Equartey Equartey self-assigned this Jul 17, 2024
@Jordan-Nelson
Copy link
Member

From offline discussions, it sounds like the issue here is caused by configuring Amplify from multiple isolates. This is not something we officially support. We use Amplify.reset() internally to release resources in tests, but it does not appear that this frees up all (or even most) of the memory. Even if the memory issue were resolved, there could be other issues you find from running multiple amplify instances in multiple isolates. I am not sure that all the interactions with device storage would be safe if executed from multiple isolates in parallel. We could create a feature request to support this. I am not certain of the level of effort for this though.

The best solution here is probably to find a way to only configure and call Amplify on a single isolate (ideally the main isolate). I think you could could use a receive port and send port to send messages between the main isolate and the new isolate that is spun up each time. Please see https://dart.dev/language/isolates#sending-multiple-messages-between-isolates-with-ports.

As for the differences between Firebase and Amplify - I cannot speak in detail about Firebase, but at a high level Firebase in Flutter is just a wrapper around a set of native iOS/Android/JS Firebase libraries. It likely doesn't matter how many Dart isolates you spin up since there is likely very little resources consumed on the dart side. Amplify Flutter is written primarily in Dart. Native code is only invoked when needed to interact with native APIs. We favor a dart first approach since it allows us to support all platforms that Flutter supports, ensures consistent behavior across those platforms, is the language that most of our customers are most familiar with, and it makes debugging much simpler.

Let us know if you have any questions.

@Jordan-Nelson Jordan-Nelson added the pending-community-response Pending response from the issue opener or other community members label Jul 25, 2024
@Jordan-Nelson
Copy link
Member

@kamami - Let me know if you have any further questions, or if you are interested in opening a feature request to track official support for using Amplify from multiple isolates.

@Jordan-Nelson Jordan-Nelson added the pending-close-response-required The issue will be closed if details necessary to reproduce the issue are not provided within 7 days. label Aug 8, 2024
@Jordan-Nelson
Copy link
Member

@kamami I am going to close this issue. We have opened #5302 to track interest in using isolates

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Issues related to the Amplify Core Plugin pending-close-response-required The issue will be closed if details necessary to reproduce the issue are not provided within 7 days. pending-community-response Pending response from the issue opener or other community members question A question about the Amplify Flutter libraries
Projects
None yet
Development

No branches or pull requests

3 participants