From 0d0471363ff07316bdced08df6a48f8fc13951ee Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 26 Jul 2019 15:59:18 -0700 Subject: [PATCH] test scenario_app on CI (flutter/engine#10065) --- engine/src/flutter/.cirrus.yml | 31 +++++-- engine/src/flutter/ci/docker/build/Dockerfile | 13 ++- engine/src/flutter/ci/firebase_testlab.sh | 37 ++++++++ .../scenario_app/android/app/build.gradle | 1 + .../android/app/src/main/AndroidManifest.xml | 6 +- .../dev/flutter/scenarios/MainActivity.java | 86 ++++++++++++++++++- .../scenario_app/compile_android_aot.sh | 1 + .../testing/scenario_app/lib/main.dart | 37 +++++++- 8 files changed, 202 insertions(+), 10 deletions(-) create mode 100755 engine/src/flutter/ci/firebase_testlab.sh diff --git a/engine/src/flutter/.cirrus.yml b/engine/src/flutter/.cirrus.yml index ce4ddab847..ee9688c4b3 100644 --- a/engine/src/flutter/.cirrus.yml +++ b/engine/src/flutter/.cirrus.yml @@ -16,10 +16,11 @@ task: FLUTTER_ENGINE: "/tmp/clean_engine/src" FRAMEWORK_PATH: "/tmp/master_framework" PATH: "$FLUTTER_ENGINE/third_party/dart/tools/sdks/dart-sdk/bin:$DEPOT_TOOLS:$PATH" + USE_ANDROID: "False" setup_script: | git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git $DEPOT_TOOLS mkdir -p $ENGINE_PATH/src - echo 'solutions = [{"managed": False,"name": "src/flutter","url": "git@github.com:flutter/engine.git","deps_file": "DEPS", "custom_vars": {"download_android_deps" : False, "download_windows_deps" : False,},},]' > $ENGINE_PATH/.gclient + echo 'solutions = [{"managed": False,"name": "src/flutter","url": "git@github.com:flutter/engine.git","deps_file": "DEPS", "custom_vars": {"download_android_deps" : ' $USE_ANDROID ', "download_windows_deps" : False,},},]' > $ENGINE_PATH/.gclient cd $ENGINE_PATH/src rm -rf flutter rm -rf out @@ -62,10 +63,9 @@ task: cd $ENGINE_PATH/src ./flutter/testing/run_tests.sh host_release - name: build_and_test_android_unopt_debug - get_android_sdk_script: | - echo 'solutions = [{"managed": False,"name": "src/flutter","url": "git@github.com:flutter/engine.git","deps_file": "DEPS", "custom_vars": {"download_windows_deps" : False,},},]' > $ENGINE_PATH/.gclient - cd $ENGINE_PATH/src - gclient sync + env: + USE_ANDROID: "True" + ANDROID_HOME: $ENGINE_PATH/src/third_party/android_tools/sdk lint_host_script: | cd $ENGINE_PATH/src/flutter/tools/android_lint $ENGINE_PATH/src/third_party/dart/tools/sdks/dart-sdk/bin/pub get @@ -77,6 +77,27 @@ task: mkdir javadoc_tmp ./flutter/tools/gen_javadoc.py --out-dir javadoc_tmp test_android_script: cd $ENGINE_PATH/src && python ./flutter/testing/run_tests.py --type=java + - name: build_and_test_android_profile_app + env: + USE_ANDROID: "True" + ANDROID_HOME: $ENGINE_PATH/src/third_party/android_tools/sdk + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[3be31b3547bea4e70cff1d46f9a11ad8c6b42c1982a3964d81e437dee2035f674f12e130bde231352421d8de2029c55f] + compile_host_script: | + cd $ENGINE_PATH/src + ./flutter/tools/gn --runtime-mode=profile --no-lto + autoninja -C out/host_profile + compile_android_script: | + cd $ENGINE_PATH/src + ./flutter/tools/gn --android --runtime-mode=profile --no-lto --android-cpu=arm64 + ninja -C out/android_profile_arm64 + compile_app_script: | + cd $ENGINE_PATH/src/flutter/testing/scenario_app + ./compile_android_aot.sh "$ENGINE_PATH/src/out/host_profile" "$ENGINE_PATH/src/out/android_profile_arm64/clang_x64" + cd android + ./gradlew assembleDebug + firebase_test_script: | + cd $ENGINE_PATH/src + ./flutter/ci/firebase_testlab.sh "$ENGINE_PATH/src/flutter/testing/scenario_app/android/app/build/outputs/apk/debug/app-debug.apk" - name: format_and_dart_test format_script: | cd $ENGINE_PATH/src/flutter diff --git a/engine/src/flutter/ci/docker/build/Dockerfile b/engine/src/flutter/ci/docker/build/Dockerfile index 890b65a588..a2573724f8 100644 --- a/engine/src/flutter/ci/docker/build/Dockerfile +++ b/engine/src/flutter/ci/docker/build/Dockerfile @@ -4,7 +4,7 @@ ENV DEPOT_TOOLS_PATH $HOME/depot_tools ENV ENGINE_PATH $HOME/engine RUN apt-get update -RUN apt-get install -y git wget curl unzip python lsb-release sudo +RUN apt-get install -y git wget curl unzip python lsb-release sudo apt-transport-https RUN git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git $DEPOT_TOOLS_PATH ENV PATH $PATH:$DEPOT_TOOLS_PATH @@ -18,3 +18,14 @@ WORKDIR $ENGINE_PATH/src RUN ./build/install-build-deps.sh --no-prompt RUN ./build/install-build-deps-android.sh --no-prompt RUN ./flutter/build/install-build-deps-linux-desktop.sh + + # Add repo for gcloud sdk and install it +RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | \ + tee -a /etc/apt/sources.list.d/google-cloud-sdk.list + + RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ + apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - + + RUN apt-get update && apt-get install -y google-cloud-sdk && \ + gcloud config set core/disable_usage_reporting true && \ + gcloud config set component_manager/disable_update_check true diff --git a/engine/src/flutter/ci/firebase_testlab.sh b/engine/src/flutter/ci/firebase_testlab.sh new file mode 100755 index 0000000000..cb2c33332c --- /dev/null +++ b/engine/src/flutter/ci/firebase_testlab.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -e + +GIT_REVISION=$(git rev-parse HEAD) + +if [[ ! -f $1 ]]; then + echo "File $1 not found." + exit -1 +fi + +# New contributors will not have permissions to run this test - they won't be +# able to access the service account information. We should just mark the test +# as passed - it will run fine on post submit, where it will still catch +# failures. +# We can also still make sure that building a release app bundle still works. +if [[ $GCLOUD_FIREBASE_TESTLAB_KEY == ENCRYPTED* ]]; then + echo "This user does not have permission to run this test." + exit 0 +fi + +echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json +gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json +gcloud --quiet config set project flutter-infra + +# Run the test. +# game-loop tests are meant for OpenGL apps. +# This type of test will give the application a handle to a file, and +# we'll write the timeline JSON to that file. +# See https://firebase.google.com/docs/test-lab/android/game-loop +gcloud firebase test android run \ + --type game-loop \ + --app $1 \ + --timeout 2m \ + --results-bucket=gs://flutter_firebase_testlab \ + --results-dir=engine_scenario_test/$GIT_REVISION/$CIRRUS_BUILD_ID + diff --git a/engine/src/flutter/testing/scenario_app/android/app/build.gradle b/engine/src/flutter/testing/scenario_app/android/app/build.gradle index 8dee894a27..473a71e1f0 100644 --- a/engine/src/flutter/testing/scenario_app/android/app/build.gradle +++ b/engine/src/flutter/testing/scenario_app/android/app/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support:design:28.0.0' + implementation 'android.arch.lifecycle:common-java8:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/engine/src/flutter/testing/scenario_app/android/app/src/main/AndroidManifest.xml b/engine/src/flutter/testing/scenario_app/android/app/src/main/AndroidManifest.xml index 87eef225a8..039498df0e 100644 --- a/engine/src/flutter/testing/scenario_app/android/app/src/main/AndroidManifest.xml +++ b/engine/src/flutter/testing/scenario_app/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ - + + + + + diff --git a/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java b/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java index c288494438..9b79fa9fc0 100644 --- a/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java +++ b/engine/src/flutter/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java @@ -1,12 +1,94 @@ package dev.flutter.scenarios; +import android.Manifest; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; -import io.flutter.app.FlutterActivity; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import io.flutter.Log; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.android.FlutterFragment; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryCodec; + +public class MainActivity extends FlutterActivity implements OnFirstFrameRenderedListener { + final static String TAG = "Scenarios"; -public class MainActivity extends FlutterActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final Intent launchIntent = getIntent(); + if ("com.google.intent.action.TEST_LOOP".equals(launchIntent.getAction())) { + if(Build.VERSION.SDK_INT > 22){ + requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); + } + // Run for one minute, get the timeline data, write it, and finish. + final Uri logFileUri = launchIntent.getData(); + new Handler().postDelayed(() -> writeTimelineData(logFileUri), 20000); + } + } + + public void onFirstFrameRendered() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + reportFullyDrawn(); + } + } + + private FlutterShellArgs getFlutterShellArgs() { + FlutterShellArgs args = FlutterShellArgs.fromIntent(getIntent()); + args.add(FlutterShellArgs.ARG_TRACE_STARTUP); + args.add(FlutterShellArgs.ARG_ENABLE_DART_PROFILING); + args.add(FlutterShellArgs.ARG_VERBOSE_LOGGING); + + return args; + } + + @Override + @NonNull + protected FlutterFragment createFlutterFragment() { + return new FlutterFragment.Builder() + .dartEntrypoint(getDartEntrypoint()) + .initialRoute(getInitialRoute()) + .appBundlePath(getAppBundlePath()) + .flutterShellArgs(getFlutterShellArgs()) + .renderMode(FlutterView.RenderMode.surface) + .transparencyMode(FlutterView.TransparencyMode.opaque) + .shouldAttachEngineToActivity(true) + .build(); + } + + private void writeTimelineData(Uri logFile) { + if (logFile == null) { + throw new IllegalArgumentException(); + } + if (getFlutterEngine() == null) { + Log.e(TAG, "Could not write timeline data - no engine."); + return; + } + final BasicMessageChannel channel = new BasicMessageChannel<>( + getFlutterEngine().getDartExecutor(), "write_timeline", BinaryCodec.INSTANCE); + channel.send(null, (ByteBuffer reply) -> { + try { + final FileDescriptor fd = getContentResolver() + .openAssetFileDescriptor(logFile, "w").getFileDescriptor(); + final FileOutputStream outputStream = new FileOutputStream(fd); + outputStream.write(reply.array()); + outputStream.close(); + } catch (IOException ex) { + Log.e(TAG, "Could not write timeline file: " + ex.toString()); + } + finish(); + }); } } diff --git a/engine/src/flutter/testing/scenario_app/compile_android_aot.sh b/engine/src/flutter/testing/scenario_app/compile_android_aot.sh index f8d1f06a24..b612fdd86f 100755 --- a/engine/src/flutter/testing/scenario_app/compile_android_aot.sh +++ b/engine/src/flutter/testing/scenario_app/compile_android_aot.sh @@ -42,6 +42,7 @@ echo "Compiling ELF Shared Library..." "$DEVICE_TOOLS/gen_snapshot" --deterministic --snapshot_kind=app-aot-elf --elf="$OUTDIR/libapp.so" --strip "$OUTDIR/app.dill" mkdir -p "android/app/src/main/jniLibs/arm64-v8a" +mkdir -p "android/app/libs" cp "$OUTDIR/libapp.so" "android/app/src/main/jniLibs/arm64-v8a/" cp "$DEVICE_TOOLS/../flutter.jar" "android/app/libs/" diff --git a/engine/src/flutter/testing/scenario_app/lib/main.dart b/engine/src/flutter/testing/scenario_app/lib/main.dart index 05dbf3ccb4..db70795546 100644 --- a/engine/src/flutter/testing/scenario_app/lib/main.dart +++ b/engine/src/flutter/testing/scenario_app/lib/main.dart @@ -3,6 +3,9 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:developer' as developer; +import 'dart:io'; +import 'dart:isolate'; import 'dart:typed_data'; import 'dart:ui'; @@ -27,7 +30,7 @@ void main() { window.sendPlatformMessage('scenario_status', data, null); } -void _handlePlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) { +Future _handlePlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) async { if (name == 'set_scenario' && data != null) { final String scenarioName = utf8.decode(data.buffer.asUint8List()); final Scenario candidateScenario = _scenarios[scenarioName]; @@ -40,9 +43,41 @@ void _handlePlatformMessage(String name, ByteData data, PlatformMessageResponseC data.setUint8(0, candidateScenario == null ? 0 : 1); callback(data); } + } else if (name == 'write_timeline') { + final String timelineData = await _getTimelineData(); + callback(Uint8List.fromList(utf8.encode(timelineData)).buffer.asByteData()); } } +Future _getTimelineData() async { + final String isolateId = developer.Service.getIsolateID(Isolate.current); + final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); + final Uri cpuProfileTimelineUri = info.serverUri.resolve( + '_getCpuProfileTimeline?tags=None&isolateId=$isolateId', + ); + final Uri vmServiceTimelineUri = info.serverUri.resolve('getVMTimeline'); + final Map cpuTimelineJson = await _getJson(cpuProfileTimelineUri); + final Map vmServiceTimelineJson = await _getJson(vmServiceTimelineUri); + final Map cpuResult = cpuTimelineJson['result'].cast(); + final Map vmServiceResult = vmServiceTimelineJson['result'].cast(); + + return json.encode({ + 'stackFrames': cpuResult['stackFrames'], + 'traceEvents': [...cpuResult['traceEvents'], ...vmServiceResult['traceEvents']], + }); +} + +Future> _getJson(Uri uri) async { + final HttpClient client = HttpClient(); + final HttpClientRequest request = await client.getUrl(uri); + final HttpClientResponse response = await request.close(); + if (response.statusCode > 299) { + return null; + } + final String data = await utf8.decodeStream(response); + return json.decode(data); +} + void _onBeginFrame(Duration duration) { _currentScenario.onBeginFrame(duration); }