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

IOS datastore query causing performance drop/lag and slow

Open Kam5678 opened this issue 7 months ago • 12 comments

Description

When I run the following function:

  Future<List<Questionnaire>> getUserQuestionnaireHistoryByName(
      String userID, String questionnaireName) {
    final questionnaireList = Amplify.DataStore.query(
      Questionnaire.classType,
      where: Questionnaire.USERID
          .eq(userID)
          .and(Questionnaire.QUESTIONNAIRENAME.eq(questionnaireName)),
      sortBy: [Questionnaire.RESPONSEDATE.descending()],
    );

    return questionnaireList;
  }

On IOS I get a performance issue, where the whole app briefly lags, while on Android I don't run into this issue at all. This performance issue happens, because I have a button and when the button is clicked, we immediately transition to a new page, where when that page is being loaded up, there is defn some lag and frame drop, when running a amplify datastore query during post frame, which also takes a while.

Categories

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

Steps to Reproduce

Main.dart build function

@override
  Widget build(BuildContext context) {
    super.build(context);
    pageList = [const PrivacyPage(), const TermsPage()];
    return 
      PopScope(
        canPop: false,
        onPopInvoked: (didPop) {
          setState(() {
            _selectedIndex = 0;
          });
        },
        child: Scaffold(
          backgroundColor: FlavorColor.backgroundSpineScaffoldColor,
          body: pageList[_selectedIndex],
          bottomNavigationBar: 
          BottomNavigationBar(
                    unselectedItemColor: Colors.grey.shade400,
        selectedItemColor:
            FlavorColor.HOME_SCREEN_SELECTED_BOTTOMBAR_TEXT_COLOR,
            onTap: _onItemClicked,
            items: [BottomNavigationBarItem(icon: Icon(Icons.home), label: "home"),
          BottomNavigationBarItem(icon: Icon(Icons.local_police_outlined), label: "loading"),])
        ),
      );
  }

Privacy page:

import 'package:ap_sci_app/helper/locator.dart';
import 'package:ap_sci_app/helper/start_up_service.dart';
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    final StartUpSharedPreferenceService _suSharedPrefService =
        getIt<StartUpSharedPreferenceService>();
    final bool useMobileLayout = MediaQuery.of(context).size.shortestSide < 600;
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
      Center(child:Text("Home", style: TextStyle(color: Colors.white)))
    ]);
  }
}

Terms Page

import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:ap_sci_app/backend_features/auth/aws_auth_functions.dart';
import 'package:ap_sci_app/helper/locator.dart';
import 'package:ap_sci_app/helper/start_up_service.dart';
import 'package:ap_sci_app/models/Questionnaire.dart';
import 'package:ap_sci_app/screens/settings/profile/sci_helper.dart';
import 'package:flutter/material.dart';

class TermsPage extends ConsumerStatefulWidget {
  const TermsPage({super.key});

  @override
  ConsumerState<TermsPage> createState() => _TermsPageState();
}

class _TermsPageState extends ConsumerState<TermsPage> {
  final StartUpSharedPreferenceService _suSharedPrefService =
      getIt<StartUpSharedPreferenceService>();

  Future<List<Questionnaire>> getUserQuestionnaireHistoryByName(
      String userID, String questionnaireName) async{
    final questionnaireList = await Amplify.DataStore.query(
      Questionnaire.classType,
      where: Questionnaire.USERID
          .eq(userID)
          .and(Questionnaire.QUESTIONNAIRENAME.eq(questionnaireName)),
      sortBy: [Questionnaire.RESPONSEDATE.descending()],
    );

    return questionnaireList;
  }

  Future<void> questionnaireFunction() async {
    String userID = await getUserID();
    await getUserQuestionnaireHistoryByName(
        userID, _suSharedPrefService.dailyCheckInName);
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      await questionnaireFunction();
      setState(() {
        done = true;
      });
    });
    
  }

  bool done = false;
  @override
  Widget build(BuildContext context) {
    final bool useMobileLayout = MediaQuery.of(context).size.shortestSide < 600;
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
      Center(child:Text(done ? "Done" :"Loading", style: TextStyle(color: Colors.white)))
    ]);
  }
}

Using the files above, i have a simple program where I am just switching between two pages using bottom navigator bar, running the amplify datastore query, during a post frame callback for the loading page, and you can see a performance lag for some reason only on IOS, and it takes a really long time as well.

No issues on Android. Any possible reason/explanation why this is happening?

Screenshots

Videos: IOS: https://github.com/user-attachments/assets/15fb62ce-05e4-40a4-a9a4-a12ff9326b90

Android: https://github.com/user-attachments/assets/f7fa1b78-7b76-422d-8868-c3e864948477

Platforms

  • [x] iOS
  • [ ] Android
  • [ ] Web
  • [ ] macOS
  • [ ] Windows
  • [ ] Linux

Flutter Version

3.27.4

Amplify Flutter Version

2.6.1

Deployment Method

Amplify CLI (Gen 1)

Schema

# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
# input AMPLIFY {
#   globalAuthRule: AuthRule = { allow: public }
# } # FOR TESTING ONLY!
type Questionnaire
	@model
	@auth(
		rules: [
			{ allow: owner, ownerField: "userID" }
			{ allow: groups, groups: ["Admin", "Researcher"] }
		]
	) {
	id: ID!
	user: User @belongsTo
	responsedate: AWSDate!
	question: [Question] @hasMany
	questionnaireName: String!
	userID: String
	createdAt: AWSDateTime
	updatedAt: AWSDateTime
	status: Boolean
	project: Project @belongsTo
	researchers: [Researcher]
		@manyToMany(relationName: "ResearcherQuestionnaire")
	completionStatus: String
	questionnaireLocation: String
	questionnaireType: String
	distributionFrequency: String
	pushReminders: [String]
	schedule: Schedule @belongsTo
	activities: [Activity] @hasMany
	inPersonVisit: InPersonVisit @belongsTo
	details: AWSJSON
}

Kam5678 avatar May 14 '25 16:05 Kam5678

Hello @Kam5678, thanks for taking the time to reach out. I'm trying to replicate your backend, but the schema is missing some type definitions. Could you please provide the rest of your schema.

tyllark avatar May 15 '25 23:05 tyllark

Hi @tyllark , thank you for looking into this!!

Would it be possible for me to send the schema over privately?

Kam5678 avatar May 16 '25 15:05 Kam5678

Hello @Kam5678, I'm looking into a method for sending the schema in a secure manner. In the meantime I've been trying to reproduce the issue with a simplified versions of the missing models without any luck:

type Project @model {
	id: ID!
  	questionnaires: [Questionnaire] @hasMany
  	name: String!
}

How many entries are returned from your getUserQuestionnaireHistoryByName call on both iOS and Android? When I stored ~10,000 Questionnaires locally it was taking ~8 seconds to run Amplify.DataStore.query(Questionnaire.classType);. Even with 100-200 entries it was taking ~.2 seconds to complete which makes awaiting a DataStore query not ideal within the Flutter render cycle. While we investigate the performance of DataStore you can use a StateManagement solution such as Listenables or avoid awaiting Amplify.DataStore.query within the Flutter render cycle to make this performance issue feel more responsive.

@override
void initState() {
   super.initState();
    questionnaireFunction().then(
      (_) => setState(() {
        done = true;
      }),
    );
}

tyllark avatar May 18 '25 22:05 tyllark

Sounds good, thank you for looking into that @tyllark

So in dynamoDB, we have about 600 questionnaire entries stored in total. As for fetching, when fetching on IOS, about 120 entries are being returned, both on IOS and Android.

Android execution time is 0.01s basically instant. But IOS takes about 9 seconds, which is really weird.

Even when not awaiting, and just running the query without an await(as your code above), still run into the same problem on IOS.

From what I observed, whenever the query starts running, performance issue starts, and when query ends, performance issue ends.

I'm not sure if this is because our model is complex, but the fact that it runs completely fine on Android, is really weird.

Kam5678 avatar May 20 '25 16:05 Kam5678

Hello @Kam5678, sorry for the delayed response. Flutter doesn't implement DataStore itself, instead we wrap the Amplify Swift/Android DataStore implementations, which can lead to some discrepancies between the platforms.

  • I've been unable to reproduce the issue with ~600 entries on my end. I still want to try with your schema to see if more complex models are causing the slowdown. While schemas are typically safe to post here, you can direct message my Amazon Discord tyllark_58391 if you don't feel comfortable posting it on Github.

  • Could you please let me know if you are testing on a physical iOS phone or a simulator and if you are able to reproduce the issue on multiple devices.

  • Finally could you rerun the test on Android/iOS with the below logging code added after you configure Amplify. This will help us determine if the issue is related to syncing with DynamoDB or querying the local DataBase.

Amplify.Hub.listen(HubChannel.DataStore, (hubEvent) {
      String hubPayload = 'No HubPayload';
      switch (hubEvent.type) {
        case DataStoreHubEventType.ready:
          break; //No Payload
        case DataStoreHubEventType.networkStatus:
          final payload = hubEvent.payload as NetworkStatusEvent?;
          hubPayload = 'active: ${payload?.active}';
        case DataStoreHubEventType.subscriptionsEstablished:
          break; //No Payload
        case DataStoreHubEventType.syncQueriesStarted:
          final payload = hubEvent.payload as SyncQueriesStartedEvent?;
          hubPayload = 'models: ${payload?.models}';
        case DataStoreHubEventType.modelSynced:
          final payload = hubEvent.payload as ModelSyncedEvent?;
          hubPayload =
              'modelName: ${payload?.modelName}, isFullSync: ${payload?.isFullSync}, isDeltaSync: ${payload?.isDeltaSync}, added: ${payload?.added}, updated: ${payload?.updated}, deleted: ${payload?.deleted}';
        case DataStoreHubEventType.syncQueriesReady:
          break; //No Payload
        case DataStoreHubEventType.outboxMutationEnqueued:
          final payload = hubEvent.payload as OutboxMutationEvent?;
          hubPayload =
              'modelName: ${payload?.modelName}, elementType: ${payload?.element.model.runtimeType}';
        case DataStoreHubEventType.outboxMutationProcessed:
          final payload = hubEvent.payload as OutboxMutationEvent?;
          hubPayload =
              'modelName: ${payload?.modelName}, elementType: ${payload?.element.model.runtimeType}';
        case DataStoreHubEventType.outboxMutationFailed:
          break; //No Payload
        case DataStoreHubEventType.outboxStatus:
          final payload = hubEvent.payload as OutboxStatusEvent?;
          hubPayload = 'isEmpty: ${payload?.isEmpty}';
        case DataStoreHubEventType.subscriptionDataProcessed:
          final payload = hubEvent.payload as SubscriptionDataProcessedEvent?;
          hubPayload =
              'modelName: ${payload?.modelName}, elementType: ${payload?.element.model.runtimeType}, version: ${payload?.element.version}, lastChangedAt: ${payload?.element.deleted}, elementType: ${payload?.element.deleted}';
      }

      safePrint(
        '${DateTime.now().toUtc()} - DataStoreEvent - ${hubEvent.eventName} - $hubPayload',
      );
    });

tyllark avatar May 30 '25 19:05 tyllark

Hi @tyllark,

No worries and thank you for replying!

I sent a message over through the discord, with everything requested

  • As for the testing, I tested both on IOS emulator and physical device in debug mode and was able to reporduce the issue on both!

Kam5678 avatar Jun 02 '25 04:06 Kam5678

Hi @Kam5678, thanks for providing the requested information, I'll let @tyllark know and we will investigate further

ekjotmultani avatar Jun 05 '25 18:06 ekjotmultani

Hi @ekjotmultani ,

For sure sounds good thank you!!

Kam5678 avatar Jun 05 '25 20:06 Kam5678

Hello @Kam5678, thank you for providing the schema, I've been able to reproduce the issue on my side. The performance hit seems to be related to saving/querying types with nested types. We are still investigating and will back to you here once we have an update.

tyllark avatar Jun 10 '25 00:06 tyllark

Hi @tyllark,

Really appreciate the update and thank you for keeping me in the loop!!

Sounds good, really appreciate all the time the team is putting into resolving this!!

Kam5678 avatar Jun 10 '25 00:06 Kam5678

Hello @Kam5678, The Amplify Swift team was able to reproduce the issue on their side, so I've created an issue in their repository. Once they have a fix we will update Amplify Flutter to use the latest Amplify Swift version.

tyllark avatar Jun 30 '25 22:06 tyllark

Sounds good thanks for the update @tyllark!

Kam5678 avatar Jul 08 '25 02:07 Kam5678