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

Conversation

GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Oct 28, 2020

Description

go/flutter-aot-split
go/flutter-split-aot-instructions

For flutter/flutter#57617 and flutter/flutter#62229

This PR contains android embedder and engine shell implementations of Split AOT that handles downloading the dynamic feature module, and passing the necessary data to the runtime in order to load a dart shared library.

In the android embedder, we add DynamicFeatureManager, which provides the general API for downloading dynamic features. The flutter play store default implementation is PlayStoreDynamicFeatureManager, which interfaces with the play store to download the APK as well as load assets and load the split dart library. Third party custom implementations of DynamicFeatureManager may be provided to FlutterJNI or the FlutterEngine java class to enable custom download and load behavior.

The shell code passes down the request to download a library via PlatformView to the embedder, as well as passes the APK paths up to the runtime controller to find the lib and load it.

The boundary of this PR is engine.cc/h, where the code that connects it to the runtime implementation (in a future PR, preliminary version can be viewed at #21173) resides. I will be landing the runtime portion of this feature after more work is done on it.

The API in this PR is not meant to be permanent, but rather a concrete working step towards fully functional split AOT apps as it gets landed/developed over next few weeks.

The WIP tooling that generates the AAB bundles can be found at flutter/flutter#63773

Tests

Tests for PlayStoreDynamicFeatureManager and FlutterInjector. Shell tests need full shell implementation before they can work.

jobject jAssetManager,
jstring jAssetBundlePath) {
auto asset_manager = std::make_shared<flutter::AssetManager>();
asset_manager->PushBack(std::make_unique<flutter::APKAssetProvider>(
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this is an issue, but this will potentially lose any other asset resolvers that were in the old asset manager (such as directories holding assets that were pushed during a hot reload)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will look into this!


public void loadAssets(@NonNull String moduleName, int loadingUnitId) {
try {
context = context.createPackageContext(context.getPackageName(), 0);
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 intentionally overwriting the context that was passed into the constructor?

Copy link
Contributor Author

@GaryQian GaryQian Oct 29, 2020

Choose a reason for hiding this comment

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

Yes. This context overwrite ensures that the newly installed dynamic feature module is present in the context when we ask for a new asset manager.

@GaryQian GaryQian force-pushed the splitaotembedder branch 3 times, most recently from 3f65733 to bc6f047 Compare October 30, 2020 00:26
@@ -469,7 +469,7 @@ deps = {
'packages': [
{
'package': 'flutter/android/embedding_bundle',
'version': 'last_updated:2020-05-20T01:36:16-0700'
Copy link
Member

Choose a reason for hiding this comment

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

I think this is all fine for a local build but you still need to update

script = "//flutter/tools/androidx/generate_pom_file.py"
to actually build it into the distributed engine artifact.

i.e. I don't think this will compile on LUCI. @jason-simmons might know more.

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 LUCI tests were able to all pass on this PR this afternoon, I'll take a look at updating it in the above mentioned spot too.

Copy link
Member

Choose a reason for hiding this comment

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

I think it'll pass on LUCI since the tests will run in the "development path". But I think it won't package up the dependencies in the vendored engine build on cloud storage.


// // |RuntimeDelegate|
// Dart_Handle Engine::OnDartLoadLibrary(intptr_t loading_unit_id) {
// return delegate_.OnDartLoadLibrary(loading_unit_id);
Copy link
Member

Choose a reason for hiding this comment

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

(I'm just reading in order so comments might not be relevant as I read the rest of the files)
Don't know what delegate is. Just a reminder to make sure this is designed generically such that it can fit through the embedder.h API in the future for other platforms.

/// @return A Dart_Handle that is Dart_Null on success, and a dart error
/// on failure.
///
virtual Dart_Handle OnDartLoadLibrary(intptr_t loading_unit_id) = 0;
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 the right abstraction to expect the embedder/embeddings to construct a Dart_Handle instance? What would you envision them to fill it with? Does it need an indirection/wrapping that's more accessible to the embedder/embeddings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll just abstract away/handle the dart_handles. This is left over from when I was doing more with the results in experiments.

/// be loaded. Notifies the engine that the requested loading
/// unit should be downloaded and loaded.
///
/// @param[in] loading_unit_id The unique id of the deferred library's
Copy link
Member

Choose a reason for hiding this comment

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

We should clarify at this level what are we loading specifically? Is it the Dart specific loading units or is it the platform-specific bundles? We should be exact with the name. i.e. in the asset-only bundle case, that asset's still loaded through the Dart API or is it a different API?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will be adding separate dart API for loading by android module name. This is currently strictly for the dart loadLibrary() code path. I'll link to additional info in a separate PR introducing the new API for module name/asset only loading.

// Called when the install request is sent successfully. This is different than a successful
// install which is handled in FeatureInstallStateUpdatedListener.
.addOnSuccessListener(
sessionId -> {
Copy link
Member

Choose a reason for hiding this comment

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

@jason-simmons you sure we should start allowing java 8 usages in our engine? We'd be forcing our users to upgrade if they were still on java 7?

Copy link
Member

Choose a reason for hiding this comment

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

The engine embedder code is already using Java lambdas, and the engine build scripts link to an Android lambda support library

Copy link
Member

Choose a reason for hiding this comment

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

Oh interesting. You're saying there are ways to support this without java 8?


public void loadDartLibrary(String moduleName, int loadingUnitId) {
// This matches/depends on dart's loading unit naming convention, which we use unchanged.
String aotSharedLibraryName = "app.so-" + loadingUnitId + ".part.so";
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 if we can hardcode this name. We probably want to fix flutter/flutter#42214 at some point. I think there are other dupes but I can't find them. Also is this name right for google3?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe the "app" in this name is not in reference to the android dir "app" module. It is the default .so name outputted by gen_snapshot. @rmacnak-google can you confirm?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In any case, I can come up with any naming scheme, this will likely change in the future as the tooling becomes more complete.

// Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64
String abi;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abi = Build.SUPPORTED_ABIS[0];
Copy link
Member

Choose a reason for hiding this comment

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

why this?

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 apparently the recommended way to obtain the ABI in lollipop+. The other way was deprecated in favor of this.

flutterJNI.loadDartLibrary(
loadingUnitId,
aotSharedLibraryName,
apkPaths.toArray(new String[apkPaths.size()]),
Copy link
Member

Choose a reason for hiding this comment

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

whatever's reading specific files out of the APK, we should do it here in this file or at least somewhere in java since it's android specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(will move to android-engine, see above comments)

@xster
Copy link
Member

xster commented Nov 3, 2020

Tagging flutter/flutter#57617

@xster
Copy link
Member

xster commented Nov 6, 2020

Let me know when you're ready for another round.

@GaryQian
Copy link
Contributor Author

GaryQian commented Nov 6, 2020

Yep, working on getting the asset manager to persist the other asset resolvers instead of overwriting it. Will let you know when ready

@GaryQian GaryQian requested a review from xster November 7, 2020 03:10
@GaryQian
Copy link
Contributor Author

GaryQian commented Nov 7, 2020

cc @xster should be ready for another round!

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.

Sorry, reviewed halfway. Will finish the rest tonight. There's also still some leftover comments from last time it seems.

@@ -469,7 +469,7 @@ deps = {
'packages': [
{
'package': 'flutter/android/embedding_bundle',
'version': 'last_updated:2020-05-20T01:36:16-0700'
Copy link
Member

Choose a reason for hiding this comment

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

I think it'll pass on LUCI since the tests will run in the "development path". But I think it won't package up the dependencies in the vendored engine build on cloud storage.

@@ -767,6 +777,27 @@ class Engine final : public RuntimeDelegate,
///
const std::string& InitialRoute() const { return initial_route_; }

//--------------------------------------------------------------------------
/// @brief Loads the dart shared library into the dart VM. When the
Copy link
Member

Choose a reason for hiding this comment

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

Is the verb right? Does the implementation of this function execute the act of "loading" or does it just signal that it can be consumed now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess it depends on what you would consider "loading". If you consider dlopen-ing a file and resolving symbols loading, then indeed it just passes the results of that on. On the other hand, this method does directly call Dart_DeferredLoadComplete, which "loads" the symbols into the running isolate. In this case, this method does invoke loading with the provided symbols.

Copy link
Member

Choose a reason for hiding this comment

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

ya, I just realized my understanding was incorrect. I think in your diagram, there's actually another bubble somewhere in the Runtime/VM box that's for "consuming the bytes than represent the AOT program" like how you have a small bubble for dlopen etc.

In that case, maybe this function should just be called loadDartDeferredLibrary too since the implementation behind this function (the vm) loads the program given to it.

/// @brief Loads the dart shared library into the dart VM. When the
/// dart library is loaded successfully, the dart future
/// returned by the originating loadLibrary() call completes.
/// Each shared library is a loading unit, which consists of
Copy link
Member

Choose a reason for hiding this comment

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

I didn't understand what this sentence meant to communicate :) It describes the what but the why might be missing

/// deferred libraries that can be compiled split from the
/// base dart library by gen_snapshot.
///
/// @param[in] loading_unit_id The unique id of the deferred library's
Copy link
Member

Choose a reason for hiding this comment

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

Explicitly state this loading unit id's relationship with the previous request call? Or are they independent?

Copy link
Member

Choose a reason for hiding this comment

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

You didn't address this one. The 2 functions' docs should explicitly cross-reference each other.

* loadingUnitId or moduleName must be valid or non-null.
*
* @param loadingUnitId The unique identifier associated with a dart deferred library. This id is
* assigned by gen_snapshot and can be referenced via bundle_config.yaml.
Copy link
Member

Choose a reason for hiding this comment

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

reference the front facing flutter build rather than gen_snapshot.

* Passing a loadingUnitId larger than the highest valid loading unit's id will
* cause the dart loadLibrary() to complete with a failure.
*
* @param moduleName The dynamic feature module name as defined in bundle_config.yaml. Flutter's
Copy link
Member

Choose a reason for hiding this comment

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

Be a bit more explicit in terms of who would call these variants specifically in what combination. i.e. the dart side API loadLibrary leads to a call with a loadingUnitId value based on what output file from gen_snapshot? We currently expect the Java application itself (or through a plugin) to call the variant with moduleName. Who calls with both parameters filled?

* cause the dart loadLibrary() to complete with a failure.
*
* @param moduleName The dynamic feature module name as defined in bundle_config.yaml. Flutter's
* default dynamic feature system stores a mapping of loadingUnitId -> moduleName
Copy link
Member

Choose a reason for hiding this comment

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

Not sure what the subclasser should do with this sentence. What are we instructing the implementer to do?

/**
* Extract and load any assets and resources from the module for use by Flutter.
*
* <p>Assets shoud be loaded before the dart derferred library is loaded, as successful loading of
Copy link
Member

Choose a reason for hiding this comment

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

typo

* however, the two parameters are still present for custom implementations that store assets
* outside of Android's native system.
*
* @param loadingUnitId The unique identifier associated with a dart deferred library.
Copy link
Member

Choose a reason for hiding this comment

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

add another paragraph to describe who should call this

* <p>Assets shoud be loaded before the dart derferred library is loaded, as successful loading of
* the dart loading unit indicates the dynamic feature is fully loaded.
*
* <p>Depending on the structure of the feature module, there may or may not be assets to extract.
Copy link
Member

Choose a reason for hiding this comment

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

Up until this point, it's a bit vague in terms of what the implementer's minimum responsibility should be. i.e. what new assumptions does the engine make after you call loadDartDeferredLibrary through JNI again? Seems like it's just updateAssetManager. If so, make it explicit.

* modules do not have an associated loadingUnitId. Instead, an invalid ID like -1 may be passed to
* download only with moduleName. On the other hand, it can be possible to resolve the moduleName based
* on the loadingUnitId. This resolution is done if moduleName is null. At least one of
* loadingUnitId or moduleName must be valid or non-null.
Copy link
Member

Choose a reason for hiding this comment

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

it's not very clear what the developer implementer should do

}

private String loadingUnitIdToModuleName(int loadingUnitId) {
// Loading unit id to module name mapping stored in android Strings
Copy link
Member

Choose a reason for hiding this comment

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

is it? How does that happen? :)

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 is stored by the tooling in the build process. This may change if/when I come up with a better way to accomplish this.

exception -> {
switch (((SplitInstallException) exception).getErrorCode()) {
case SplitInstallErrorCode.NETWORK_ERROR:
flutterJNI.dynamicFeatureInstallFailure(
Copy link
Member

Choose a reason for hiding this comment

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

does this overlap with the SplitInstallStateUpdatedListener callback above? i.e. could dynamicFeatureInstallFailure happen twice per request?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, these errors indicate the install request was never registered/started. The errors above indicate a properly fired request has failed.

}

public void uninstallFeature(int loadingUnitId, String moduleName) {
// TODO(garyq): support uninstalling.
Copy link
Member

Choose a reason for hiding this comment

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

if you're not supporting it, let's not add it to the interface yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just wanted to clarify that the TODOs I'm adding in this PR are meant to be actually implemented shortly. They usually indicate critical components of the feature that may just be a bit too big or out of scope for a single PR, and will be landed before the feature is considered ready.

Copy link
Member

Choose a reason for hiding this comment

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

SG

handle = ::dlopen(path.c_str(), RTLD_NOW);
search_paths.pop_back();
if (handle == nullptr) {
FML_LOG(ERROR) << "No dart shared library found at \"" << path
Copy link
Member

Choose a reason for hiding this comment

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

are we expecting failures for the first entries of the searchpaths? i.e. is this spammy?

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 do expect some failures, I'll remove this log.


// Resolve symbols.
uint8_t* isolate_data =
static_cast<uint8_t*>(::dlsym(handle, DartSnapshot::kIsolateDataSymbol));
Copy link
Member

Choose a reason for hiding this comment

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

if we're going through all the effort of recreating this, it's then not clear why aren't we just using the fml library to start with?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I currently still want explicit control over this process as there are minor nuances that are done differently, I expect to refactor this to use existing FML once the process is more locked down.

@xster
Copy link
Member

xster commented Nov 10, 2020

I think all my comments are minor. Generally LGTM. I think this PR's ready for tests

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.

LGTM! Woohoo!

@@ -5,7 +5,9 @@
package io.flutter;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
Copy link
Member

Choose a reason for hiding this comment

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

Sorry I made a mistake here. If you sync to head, these should be androidx dependencies. The new one should be androidx too.

}

public void destroy() {
splitInstallManager.unregisterListener(listener);
Copy link
Member

Choose a reason for hiding this comment

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

while you're here, might as well remove the JNI reference. i.e. during the destruction sequence, there is no guarantee that there won't be a race where a load asset platform channel call lands right as some items are being destroyed.

Copy link
Member

Choose a reason for hiding this comment

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

You didn't address this one.

@@ -374,6 +376,7 @@ public void destroy() {
platformViewsController.onDetachedFromJNI();
dartExecutor.onDetachedFromJNI();
flutterJNI.removeEngineLifecycleListener(engineLifecycleListener);
flutterJNI.setDynamicFeatureManager(null);
flutterJNI.detachFromNativeAndReleaseResources();
Copy link
Member

Choose a reason for hiding this comment

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

if FlutterInjector.instance().dynamicFeatureManager() is not null, call destroy

Copy link
Member

Choose a reason for hiding this comment

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

You didn't address this one. I think nothing's calling DynamicFeatureManager.destroy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Github UI didn't register this one changing as well, see lines below.

public void requestDartDeferredLibrary(int loadingUnitId) {
if (dynamicFeatureManager != null) {
dynamicFeatureManager.downloadDynamicFeature(loadingUnitId, null);
}
Copy link
Member

Choose a reason for hiding this comment

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

Since this is where the rubber meets the road, let's show a scary message here so it's easier to debug. i.e. you used a split aot feature by calling the dart loadLibrary function but didn't set a manager. Please see integration doc: .... Split AOT will not work unless you complete integration from native side.

Copy link
Member

Choose a reason for hiding this comment

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

You didn't address this one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did, the Github UI just didn't register it as outdated since the comment is on a } :)

* loading Dart deferred libraries. A typical code-flow begins with a Dart call to loadLibrary() on
* deferred imported library. See https://dart.dev/guides/language/language-tour#deferred-loading
* This call retrieves a unique identifier called the loading unit id, which is assigned by
* gen_snapshot during compilation. The loading unit id is passed down through the engine and
Copy link
Member

Choose a reason for hiding this comment

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

add a line to say where the user should look to find that id

* <p>Flutter dynamic feature support is still in early developer preview and should not be used in
* production apps yet.
*
* <p>The Flutter default implementation is PlayStoreDynamicFeatureManager.
Copy link
Member

Choose a reason for hiding this comment

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

nit (mainly for this class because it's our "main" java doc): use {@link ...} everywhere in this file when referring to other java classes. Use full qualifiers to avoid class imports if needed. Use {@code ...} to refer to non-java code.

{@link #method...} can reference class methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'll go ahead and go through and add all the links.

* is fully initialized, this method should be called to provide the FlutterJNI instance to use
* for use in loadDartLibrary and loadAssets.
*/
public abstract void setJNI(FlutterJNI flutterJNI);
Copy link
Member

Choose a reason for hiding this comment

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

yak creep / optional: one way you can get around this is letting the FlutterJNI be in the injector too. Then you can just read from the injector right before use.

*/
public abstract void uninstallFeature(int loadingUnitId, String moduleName);

/** Destructor that cleans up any leftover objects that are no longer needed. */
Copy link
Member

Choose a reason for hiding this comment

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

nit on technical wording: Java doesn't have destructors (something that immediately prompts for memory to be release) since it's garbage collected. The best we can say is calling this releases all its resources. After calling this, this object is no longer usable.


private boolean verifyJNI() {
if (flutterJNI == null) {
Log.e(TAG, "No FlutterJNI provided.");
Copy link
Member

Choose a reason for hiding this comment

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

I think this appears without context. Be a bit more detailed. You must set this to integrate split modules before calling dart apis or loading assets via platform channel.

@GaryQian GaryQian added the waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. label Nov 20, 2020
@GaryQian GaryQian merged commit 53fc019 into flutter:master Nov 20, 2020
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Nov 20, 2020
renyou added a commit that referenced this pull request Nov 20, 2020
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Nov 20, 2020
jason-simmons pushed a commit to flutter/flutter that referenced this pull request Nov 24, 2020
* 53fc019 Split AOT Android Embedder and shell (flutter/engine#22179)

* fc55814 Implement Scene.toImage() in CanvasKit mode. (flutter/engine#22085)

* c45e02a Roll Dart SDK from 12fded61a2bc to a06d469024fd (1 revision) (flutter/engine#22623)

* 550c750 Remove opt outs for dart:ui (flutter/engine#22603)

* f2803ac [fuchsia] shader warmup fixes (flutter/engine#22439)

* ce94c4e Roll Dart SDK from a06d469024fd to b8fea79a2549 (1 revision) (flutter/engine#22630)

* 76b6acb Roll Fuchsia Linux SDK from aAb3NJv_h... to X1ue-JZsc... (flutter/engine#22631)

* 976e887 Roll Skia from ed289e777cfa to 9dce4d081f8a (3 revisions) (flutter/engine#22632)

* 885bd65 Roll Fuchsia Mac SDK from DQpWjEN59... to wGZWtwuY4... (flutter/engine#22633)

* 8971b82 Roll Dart SDK from b8fea79a2549 to 861ebcb175b6 (1 revision) (flutter/engine#22634)

* a09cdfd Roll Skia from 9dce4d081f8a to 8c5889937172 (1 revision) (flutter/engine#22635)

* a9f332c Roll Dart SDK from 861ebcb175b6 to 1adf3d5fa9d0 (1 revision) (flutter/engine#22636)

* 1bf5c8b [web] Implement tilemode for gradient shaders. (flutter/engine#22597)

* 97cacfb Add more runtime intrinsic symbols to the export checker script (flutter/engine#22641)
chaselatta pushed a commit to chaselatta/engine that referenced this pull request Nov 30, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes platform-android platform-ios waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants