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

perf(Android): getConstants() in thread to parallelize load, speed up for most #680

Conversation

mcuelenaere
Copy link
Contributor

@mcuelenaere mcuelenaere commented Jun 3, 2019

Description

RNDeviceModule.getConstants() can take a lot of time (~300ms on a Galaxy S7), so try to run it as soon as possible in the background so that by the time it gets called, the constants have already been calculated and we can just return the map.

Checklist

  • I have tested this on a device/simulator for each compatible OS
  • I added the documentation in README.md
  • I mentioned this change in CHANGELOG.md

@noomorph
Copy link

noomorph commented Jun 3, 2019

What are your measurements (before vs after) and how did you measure time-to-load?

@mcuelenaere
Copy link
Contributor Author

mcuelenaere commented Jun 3, 2019

This was measured by adding a ReactMarker listener and printing out durations of each step, where we focus here only on the GET_CONSTANTS part of RNDeviceInfo.

Below are 3 measurements from before and after the patch, each starting from a cold environment (app was killed). As you can see, the GET_CONSTANTS duration gets reduced to almost nothing.

BEFORE:

   +325.000000 ms --                    GET_CONSTANTS [START]: RNDeviceInfo
   +229.000000 ms --                CONVERT_CONSTANTS [START]: RNDeviceInfo
     +6.000000 ms --                  CONVERT_CONSTANTS [END]: RNDeviceInfo
     +0.000000 ms --                      GET_CONSTANTS [END]: RNDeviceInfo

   +288.000000 ms --                    GET_CONSTANTS [START]: RNDeviceInfo
   +265.000000 ms --                CONVERT_CONSTANTS [START]: RNDeviceInfo
     +3.000000 ms --                  CONVERT_CONSTANTS [END]: RNDeviceInfo
     +0.000000 ms --                      GET_CONSTANTS [END]: RNDeviceInfo

   +286.000000 ms --                    GET_CONSTANTS [START]: RNDeviceInfo
   +262.000000 ms --                CONVERT_CONSTANTS [START]: RNDeviceInfo
     +4.000000 ms --                  CONVERT_CONSTANTS [END]: RNDeviceInfo
     +0.000000 ms --                      GET_CONSTANTS [END]: RNDeviceInfo

AFTER:

   +276.000000 ms --                    GET_CONSTANTS [START]: RNDeviceInfo
     +0.000000 ms --                CONVERT_CONSTANTS [START]: RNDeviceInfo
     +1.000000 ms --                  CONVERT_CONSTANTS [END]: RNDeviceInfo
     +0.000000 ms --                      GET_CONSTANTS [END]: RNDeviceInfo

   +382.000000 ms --                    GET_CONSTANTS [START]: RNDeviceInfo
     +0.000000 ms --                CONVERT_CONSTANTS [START]: RNDeviceInfo
     +1.000000 ms --                  CONVERT_CONSTANTS [END]: RNDeviceInfo
     +0.000000 ms --                      GET_CONSTANTS [END]: RNDeviceInfo

   +280.000000 ms --                    GET_CONSTANTS [START]: RNDeviceInfo
     +0.000000 ms --                CONVERT_CONSTANTS [START]: RNDeviceInfo
     +1.000000 ms --                  CONVERT_CONSTANTS [END]: RNDeviceInfo
     +0.000000 ms --                      GET_CONSTANTS [END]: RNDeviceInfo

Your mileage may vary, since this depends on how much time is in between the RNDeviceModule constructor being called and the getConstants() method being invoked (+ scheduling and thread synchronization overhead).

tl;dr: if you have a decent amount of RN modules being loaded in your app (and your device has multiple cores), this should have a positive effect.

Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice workaround! even with a move to a Promise style as desired in the next major rev (was planned for v2 but we had a breaking change prior, so v3) this might have value as a "constants warmer". I can't imagine it is common, but it is possible that converting this getConstants() time from serial to parallel may negatively impact people. I don't mind making this the default behavior, but how hard would it be to make two constructors, one that takes no arguments and is default to "warm constants" and one that allows a boolean and let's people defer the cost to first getConstants() invocation like existing behavior?

If it is too difficult I'd scrap the idea, but I think it might be easy assuming getConstants() will always fill lazily if the AsyncTask hasn't been called yet

That way for the few that may have taken great pains to avoid this cost in some other way, we won't be blowing up whatever work they've done

What do you think?

@mikehardy
Copy link
Collaborator

just marking it as "needs-changes" because that's a tag I review periodically even when PRs go to sleep

@mcuelenaere mcuelenaere force-pushed the feature/async-getconstants branch from 2f1d4c4 to 55dc03d Compare June 3, 2019 15:09
@mcuelenaere
Copy link
Contributor Author

@mikehardy the behaviour is optional now and disabled by default

@mikehardy
Copy link
Collaborator

How did I miss that? Let me re-read, because that sounds perfect...

@mikehardy
Copy link
Collaborator

Oh you just did it haha, thought I was losing my mind. This does sound perfect now, I will re-read and probably merge

Thanks for the contribution!

this(reactContext, false);
}

public RNDeviceModule(ReactApplicationContext reactContext, boolean loadConstantsAsynchronously) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it optional, but now for this method to be invoke with async load, does it need some corresponding hook in RNDeviceInfo.java? Like RNDeviceInfo needs two constructors with a similar flag, it stores the state, then when RNDeviceInfo.createNativeModules is called it sends the flag through?

Otherwise I'm not sure how the new async style is invoked since the default (and only currently available) style from linking is to just new RNDeviceInfo and there are no other control flow entrypoints to toggle the behavior https://github.com/mcuelenaere/react-native-device-info/blob/55dc03dc46/example/android/app/src/main/java/com/example/MainApplication.java#L27

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to complicate things for what is probably a non-issue statistically but I still think it's quickly achievable :-)

Copy link
Contributor Author

@mcuelenaere mcuelenaere Jun 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you're right. It's because locally I'm using Turbo Modules and thus creating the RNDeviceModule myself. I'll adapt RNDeviceInfo to also take a constructor argument.

@mikehardy
Copy link
Collaborator

I pulled this locally and have it ready for testing - I think it needs one more tweak after I requested it be optional then it's probably good to go

…time

RNDeviceModule.getConstants() can take a lot of time (~300ms on a Galaxy S7), so try
to run it as soon as possible in the background so that by the time it gets called,
the constants have already been calculated and we can just return the map.
@mcuelenaere mcuelenaere force-pushed the feature/async-getconstants branch from 55dc03d to b0f7500 Compare June 3, 2019 15:55
@mcuelenaere
Copy link
Contributor Author

@mikehardy loadConstantsAsynchronously is now propagated from RNDeviceInfo to RNDeviceModule

@mikehardy
Copy link
Collaborator

I'll repull and retest - super-speedy thanks!

@mikehardy mikehardy changed the title Android: run getConstants() in a different thread to speedup loading time perf(Android): getConstants() in thread to parallelize load, speed up for most Jun 3, 2019
@mikehardy mikehardy merged commit 2f837b9 into react-native-device-info:master Jun 3, 2019
@mikehardy
Copy link
Collaborator

https://github.com/react-native-community/react-native-device-info/releases/tag/v2.1.0 🚀

@ItsNoHax
Copy link
Contributor

@mikehardy @mcuelenaere First of all thanks a lot to both of you for the fix. I would just like to point out that the change log and the README should reflect these changes. I'm still unclear on whether I should use true or false in the constructor.

@mikehardy
Copy link
Collaborator

@ItsNoHax the release notes do mention it? https://github.com/react-native-community/react-native-device-info/blob/master/CHANGELOG.md#210 (they say use true)

I suppose the README for install could be updated, it might help to explain that the constructor even has an optional boolean argument, and what it means. Happy to take a PR for that - you can edit it directly using the GitHub UI and propose some words that you think would help people?

@ItsNoHax
Copy link
Contributor

ItsNoHax commented Jun 18, 2019

@mikehardy Done, let me know what you think: #697

I left it under the main installation header as I was afraid people might miss it when linking automatically.

@elyalvarado
Copy link
Contributor

Hi guys, how will this work with RN>=0.60 auto loader?

@jslok
Copy link
Contributor

jslok commented Jul 29, 2019

Is there a way to make this work with autolinking or do we need to manually link to use this feature? Thank you

@mikehardy
Copy link
Collaborator

@jslok to be honest I am not sure - but I am curious myself as I am just migrating my work project to RN60 now.

My current understanding is that to initialize a module with a non-default constructor (specifically: one that takes an argument) in the auto-linking world, you have to use a react-native-config.js to disable autolinking, then manually link it on the platform you disabled.

@thymikee is that correct? auto-linking only works with default constructors?

I initially merged this patch as a new option and did not set the deferred method to default but assuming my understanding of auto-linking is correct and it will only work with a default constructor, I'd accept a patch that reversed which style was default now that it has been released a while and shown to be working.

@thymikee
Copy link

@mikehardy disabling linking in react-native.config.js affects both autolinking and link.

Passing a custom constructor is possible through android.packageInstance:

module.exports = {
  dependencies: {
    'react-native-device-info': {
      platforms: {
        android: {
          packageInstance: 'new RNDeviceInfo(true)'
        }
      }
    }
  }
};

@mikehardy
Copy link
Collaborator

@thymikee I really appreciate both your responsiveness, and your spot-on response, thank you! I promise I'll be good at auto-linking soon :-)

Okay then, @jslok I would happily take either a PR that made the parallel version default + a quick doc snippet on how to disable it (with the snippet above I think but reversed to false) or a doc enhancement that had this snippet showing how to enable it.

I think it's ready for default though, I believe more will benefit from it's being enabled than will be harmed so I would prefer just enabling by default

@jslok jslok mentioned this pull request Jul 29, 2019
6 tasks
@Dror-Bar
Copy link

Dror-Bar commented Aug 4, 2019

I don't have a file named react-native.config.js. It is unclear from the readme whether I should create it myself and where.

I'm using isTablet() method to calculate UI elemens in the first screen. So I need to load it synchronically.

@mikehardy
Copy link
Collaborator

@Dror-Bar if you wanted to propose a quick readme PR that made it clear to you that would help. Specifically addressed at the confusion: the file does not exist in the repository, but if you do not want the parallel initialization, you will need to create the file, and it should contain the stanza prescribed in the react-native-config.js in the readme

If you are okay with the new default / parallel initialization is okay for your project, you do not need to do anything

My decision to merge this and alter the default was based on the assumption it was reasonably tested (so would not break anyone) and that in general it was a performance increase for everyone or at least did not harm things. If this is not true for your project I would love to know why in order to see if my assumption was correct - and I'd apologize for regressing your project also, that definitely wasn't intended

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants