Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@dnfield
Copy link
Contributor

@dnfield dnfield commented Aug 13, 2020

Android side of flutter/flutter#63631

Need to figure out how to test this, as all the surrounding code is untested 😢

@flutter-dashboard

This comment has been minimized.

@auto-assign auto-assign bot requested a review from iskakaushik August 13, 2020 08:26
@dnfield dnfield changed the title Start Limit heap growth on Android Aug 13, 2020
@dnfield dnfield marked this pull request as draft August 13, 2020 08:26
@dnfield dnfield removed the request for review from iskakaushik August 13, 2020 08:28
@dnfield dnfield marked this pull request as ready for review September 18, 2020 18:54
@auto-assign auto-assign bot requested a review from liyuqian September 18, 2020 18:54
result.appStoragePath,
result.engineCachesPath,
initTimeMillis,
oldGenHeapSizeMegaBytes);
Copy link
Member

Choose a reason for hiding this comment

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

Can oldGenHeapSizeMegaBytes be provided as a shell command line flag in shell/common/switches.h?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could but then we lose some static type checking here. Is there some reason you have in mind to do it that way?

Copy link
Member

Choose a reason for hiding this comment

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

It would be more consistent with how other engine settings are assigned and would avoid a proliferation of nativeInit arguments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

*/
@Keep
public class FlutterJNI {
public static class FlutterJNILoader {
Copy link
Member

Choose a reason for hiding this comment

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

Making all the static private SG. But rather than making this class 2 tiered which is a bit more confusing, why not just let classes that want to mock FlutterJNI just inject a FlutterJNI mock directly so all methods can be mocked?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It limits the scope of what FlutterLoader gets, making it a little easier to quickly see exactly what it expects to use and how these things are related.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IOW, writing tests for this won't leave me wondering which of the many methods on FlutterJNI I'm supposed to mock.

Copy link
Member

Choose a reason for hiding this comment

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

Jason might have the historical background. I believe we made all the methods in the FlutterJNI class grouped in one class instead of being sliced and diced per consumer based on least privilege principle for a reason (probably to have a single place to look at the interop schema). If so, I strongly believe we should have a single pattern in the codebase to be consistent. Either keep everything in one class (and remove statics which I support) or break it all up and parcel views of the JNI to its consumer based on what the consumer needs.

Otherwise, it becomes arbitrary why I could mock the FlutterLoader's JNI loader but on some JNI calls, it still crashes.

I think the cleanest way for your intended change is still to make all the statics normal instance methods for the public APIs and inject the whole FlutterJNI in tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The actual native methods are still on Flutter JNI, which still keeps it easier for us to write the JNI bindings.

I'm not really clear on why we would want to keep all of the Java facing methods on the same class. Flutter JNI will stick around as this sort of God-object, and everyone you give it to will be able to call all sorts of native methods that may not make sense.

I don't work on this code very much, so I'm not going to put up a huge fight about this. If, seeing what I'm saying, you still believe these methods should all be exposed on the Java side via the same one object, I can easily refactor this PR to do that - although if I were working on this more I would more strongly object :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Talked offline. I'll move out this part of the change here, we can track what to do with FlutterJNI overall in flutter/flutter#66131

I've also made the static methods public again, but marked them as deprecated with pointers to the instance methods.

Copy link
Member

Choose a reason for hiding this comment

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

Ping me when it's ready again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should be ready now :) The test failure is an infra flake.

*/
@Keep
public class FlutterJNI {
public static class FlutterJNILoader {
Copy link
Member

Choose a reason for hiding this comment

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

Jason might have the historical background. I believe we made all the methods in the FlutterJNI class grouped in one class instead of being sliced and diced per consumer based on least privilege principle for a reason (probably to have a single place to look at the interop schema). If so, I strongly believe we should have a single pattern in the codebase to be consistent. Either keep everything in one class (and remove statics which I support) or break it all up and parcel views of the JNI to its consumer based on what the consumer needs.

Otherwise, it becomes arbitrary why I could mock the FlutterLoader's JNI loader but on some JNI calls, it still crashes.

I think the cleanest way for your intended change is still to make all the statics normal instance methods for the public APIs and inject the whole FlutterJNI in tests.

*
* <p>Useful for testing since JVM tests via Robolectric can't load native libraries.
*/
public boolean shouldLoadNative() {
Copy link
Member

Choose a reason for hiding this comment

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

FWIW, this is a breaking change. I still haven't merged the website PR for the last time this broke so I'll hold off until we resolve this PR. flutter/website#4561

Copy link
Member

@xster xster left a comment

Choose a reason for hiding this comment

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

Thanks for making the changes.

public FlutterInjector build() {
fillDefaults();

System.out.println("should load native is " + shouldLoadNative);
Copy link
Member

Choose a reason for hiding this comment

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

🤦‍♀️ thanks


// BEGIN Methods related to loading for FlutterLoader.
/**
* Loads the libflutter.so library.
Copy link
Member

Choose a reason for hiding this comment

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

great. Thanks for adding docs


// BEGIN Methods related to loading for FlutterLoader.
/**
* Loads the libflutter.so library.
Copy link
Member

Choose a reason for hiding this comment

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

ultra-nit: Loads the libflutter.so Flutter engine native library.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

libflutter.so C++ library good?

Copy link
Member

Choose a reason for hiding this comment

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

SG

return instance;
}

public FlutterLoader() {
Copy link
Member

Choose a reason for hiding this comment

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

Docs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

this(null);
}

public FlutterLoader(@Nullable FlutterJNI flutterJNILoader) {
Copy link
Member

Choose a reason for hiding this comment

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

replace all flutterJNILoader with just flutterJNI. Since there's nothing else called jni loader here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, also made this @nonnullable and made the default ctor new one up.

ActivityManager activityManager =
(ActivityManager) applicationContext.getSystemService(Context.ACTIVITY_SERVICE);
int oldGenHeapSizeMegaBytes = activityManager.getLargeMemoryClass();
shellArgs.add("--old-gen-heap-size=" + oldGenHeapSizeMegaBytes);
Copy link
Member

Choose a reason for hiding this comment

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

Is this equality right? I assume your total memory size needs to accommodate everything right? Native memory, old gen, new gen, whatever else the VM mmaps. The old gen can't take up everything I imagine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what we have control over.

I'm probably garbling this up a little bit, but basically new space is moved to old space once it survives collection, and things are put directly into old space if they're over a certain size threshhold. So basically, the majority of heap space is reserved for old space. New space is kept smaller so that new shortlived garbage can be collected more aggressively.

This will cause Dart to run collections much more aggressively as we approach this limit. @rmacnak-google - will this be a hard limit for the VM, or will it be possible to go over it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We might want to bump this up a bit if it's a hard limit now that I think about it. Android doesn't want us to use more than this if we're good citizens, but this value might be a little lower than what's really "safe" to use in practice.

Copy link
Member

Choose a reason for hiding this comment

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

I don't know what this does behind the scene. I guess the desired behavior is probably like if we start trying to collect when it's like 60% of this value, then it's probably safe. If it collects at this value, we're probably going to have trouble.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This acts like a limit the Dart VM tries to keep us from approaching asymptotically.

But this value is much lower than the limit we use today, which is smething like 32GB (and probably more ram than any Android device out there has). The highest I've seen this value is 512gb, on phones that have ~4gb of ram.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

512gb -> 512mb

// Setup test.
FlutterInjector.setInstance(new FlutterInjector.Builder().setShouldLoadNative(false).build());
FlutterJNI mockFlutterJNILoader = mock(FlutterJNI.class);
FlutterInjector.setInstance(
Copy link
Member

Choose a reason for hiding this comment

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

Cool, lg. Can you push some changes to flutter/website#4561 directly for the new pattern?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

import android.app.ActivityManager;
import android.content.Context;
import io.flutter.embedding.engine.FlutterJNI;
import java.util.*;
Copy link
Member

Choose a reason for hiding this comment

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

import specific classes if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

FlutterInjector.reset();
verify(mockFlutterJNILoader, times(1)).loadLibrary();

ActivityManager activityManager =
Copy link
Member

Choose a reason for hiding this comment

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

can this be a separate test? Since it seems unrelated to the test above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

@xster xster left a comment

Choose a reason for hiding this comment

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

LG, thanks

/** Creates a {@code FlutterLoader} that uses a default constructed {@link FlutterJNI}. */
public FlutterLoader() {
this(null);
this(new FlutterJNI());
Copy link
Member

Choose a reason for hiding this comment

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

this is nicer

@dnfield
Copy link
Contributor Author

dnfield commented Sep 28, 2020

The more I think about this, the more I think we should make this an opt-in parameter. I'm going to change up this PR to do that. We can then look at having some clients set it to whatever level they think is appropriate.

@xster - SGTY?

@xster
Copy link
Member

xster commented Sep 29, 2020

Yes I agree

@dnfield dnfield marked this pull request as ready for review November 12, 2020 19:48
@dnfield
Copy link
Contributor Author

dnfield commented Nov 12, 2020

@xster @jason-simmons - I've refactored this to use half of total memory, or a value specified from the AndroidManifest metadata if available.

This will be more in line with what we do on iOS, and should be less surprising to customers - for example, on many devices today, half of total memory will be 2-6 times as much as the large memory class.

@chinmaygarde
Copy link
Member

I've reported the flake and re-run the presubmit. This looks good to me and already has approvals. Can we land this?

@dnfield
Copy link
Contributor Author

dnfield commented Nov 12, 2020

I think that's fine - if @xster or @jason-simmons have any concerns with the new changes we can revisit.

@xster
Copy link
Member

xster commented Nov 13, 2020

This seems like a big change. You're sure we don't want to start with just an opt-in?

@dnfield
Copy link
Contributor Author

dnfield commented Nov 13, 2020

I think it's more confusing to have this behavior on ios but not on android.

@xster
Copy link
Member

xster commented Nov 13, 2020

SG

chaselatta pushed a commit to chaselatta/engine that referenced this pull request Nov 30, 2020
Limit heap growth to half of totalMem by default, with the option to set it higher or lower in the AndroidManifest
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants