Unverified Commit a5f57b9e authored by gaaclarke's avatar gaaclarke Committed by GitHub

Added performance benchmarks for platform channels (#81414)

parent dbc88268
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: ce18d702e90d3dff9fee53d61a770c94f14f2811
channel: master
project_type: app
# platform_channels_benchmarks
The harness for running performance benchmark tests for Platform Channels.
If you want to run these benchmarks outside of devicelab you need to first run:
`flutter create --platforms="ios,android" --no-overwrite .`
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.example.platform_channels_benchmarks
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.StandardMessageCodec
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
val basicStandard = BasicMessageChannel(flutterEngine.dartExecutor, "dev.flutter.echo.basic.standard", StandardMessageCodec.INSTANCE)
basicStandard.setMessageHandler { message, reply -> reply.reply(message) }
super.configureFlutterEngine(flutterEngine)
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import Flutter
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let registrar = self.registrar(forPlugin: "Echo")!
let basicStandard = FlutterBasicMessageChannel(
name: "dev.flutter.echo.basic.standard", binaryMessenger: registrar.messenger(),
codec: FlutterStandardMessageCodec.sharedInstance())
basicStandard.setMessageHandler { (input, reply) in
reply(input)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:microbenchmarks/common.dart';
List<Object> _makeTestBuffer(int size) {
final List<Object> answer = <Object>[];
for (int i = 0; i < size; ++i) {
switch (i % 9) {
case 0:
answer.add(1);
break;
case 1:
answer.add(math.pow(2, 65));
break;
case 2:
answer.add(1234.0);
break;
case 3:
answer.add(null);
break;
case 4:
answer.add(<int>[1234]);
break;
case 5:
answer.add(<String, int>{'hello': 1234});
break;
case 6:
answer.add('this is a test');
break;
case 7:
answer.add(true);
break;
case 8:
answer.add(Uint8List(64));
break;
}
}
return answer;
}
Future<void> _runTests() async {
if (kDebugMode) {
throw Exception(
"Must be run in profile mode! Use 'flutter run --profile'.");
}
const int numMessages = 2500;
const BasicMessageChannel<Object> channel = BasicMessageChannel<Object>(
'dev.flutter.echo.basic.standard',
StandardMessageCodec(),
);
final Stopwatch watch = Stopwatch();
watch.start();
for (int i = 0; i < numMessages; ++i) {
await channel.send(1234);
}
watch.stop();
final double smallPayloadTime = watch.elapsedMicroseconds / numMessages;
watch.reset();
/// WARNING: Don't change the following line of code, it will invalidate
/// `Large` tests. Instead make a different test. The size of largeBuffer
/// serialized is 14214 bytes.
final List<Object> largeBuffer = _makeTestBuffer(1000);
int size = 0;
watch.start();
for (int i = 0; i < numMessages; ++i) {
final List<Object> result = await channel.send(largeBuffer) as List<Object>;
// This check should be tiny compared to the actual channel send/receive.
size += (result == null) ? 0 : result.length;
}
watch.stop();
final double largePayloadTime = watch.elapsedMicroseconds / numMessages;
if (size != largeBuffer.length * numMessages) {
throw Exception(
'There is an error with the echo channel, the results don\'t add up: $size');
}
final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
printer.addResult(
description: 'BasicMessageChannel/StandardMessageCodec/Flutter->Host/Small',
value: smallPayloadTime,
unit: 'µs',
name: 'platform_channel_basic_standard_2host_small',
);
printer.addResult(
description: 'BasicMessageChannel/StandardMessageCodec/Flutter->Host/Large',
value: largePayloadTime,
unit: 'µs',
name: 'platform_channel_basic_standard_2host_large',
);
printer.printToStdout();
}
class _BenchmarkWidget extends StatefulWidget {
const _BenchmarkWidget(this.tests, {Key key}) : super(key: key);
final Future<void> Function() tests;
@override
_BenchmarkWidgetState createState() => _BenchmarkWidgetState();
}
class _BenchmarkWidgetState extends State<_BenchmarkWidget> {
@override
void initState() {
widget.tests();
super.initState();
}
@override
Widget build(BuildContext context) => Container();
}
void main() {
runApp(const _BenchmarkWidget(_runTests));
}
name: platform_channels_benchmarks
description: Test harness for Platform Channel performance tests.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.9.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
microbenchmarks:
path: ../microbenchmarks
cupertino_icons: 1.0.2
_fe_analyzer_shared: 21.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
analyzer: 1.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
args: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
async: 2.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
boolean_selector: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
characters: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
charcode: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
cli_util: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.15.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
convert: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
coverage: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
crypto: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
file: 6.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
glob: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_multi_server: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 4.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
io: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
logging: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
matcher: 0.12.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
meta: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
mime: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_preamble: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
package_config: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path: 1.8.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pedantic: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pool: 1.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pub_semver: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_packages_handler: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_static: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_web_socket: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_map_stack_trace: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_maps: 0.10.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_span: 1.8.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stack_trace: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stream_channel: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
string_scanner: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
term_glyph: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test: 1.16.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_api: 0.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_core: 0.3.19 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
typed_data: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vector_math: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vm_service: 6.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
watcher: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
webkit_inspection_protocol: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
yaml: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dev_dependencies:
flutter_test:
sdk: flutter
clock: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
fake_async: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
flutter:
uses-material-design: true
# PUBSPEC CHECKSUM: 77ad
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_devicelab/framework/adb.dart'
show DeviceOperatingSystem;
import 'package:flutter_devicelab/framework/framework.dart' show task;
import 'package:flutter_devicelab/tasks/platform_channels_benchmarks.dart'
as platform_channels_benchmarks;
Future<void> main() async {
await task(
platform_channels_benchmarks.runTask(DeviceOperatingSystem.android));
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_devicelab/framework/adb.dart'
show DeviceOperatingSystem;
import 'package:flutter_devicelab/framework/framework.dart' show task;
import 'package:flutter_devicelab/tasks/platform_channels_benchmarks.dart'
as platform_channels_benchmarks;
Future<void> main() async {
await task(platform_channels_benchmarks.runTask(DeviceOperatingSystem.ios));
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;
/// Launches a new Flutter process.
Future<Process> startFlutter({
List<String> options = const <String>[],
bool canFail = false,
Map<String, String> environment,
}) {
final List<String> args = flutterCommandArgs('run', options);
return startProcess(path.join(flutterDirectory.path, 'bin', 'flutter'), args, environment: environment);
}
/// Reades through the print commands from [process] waiting for the magic phase
/// that contains microbenchmarks results as defined in
/// `dev/benchmarks/microbenchmarks/lib/common.dart`.
Future<Map<String, double>> readJsonResults(Process process) {
// IMPORTANT: keep these values in sync with dev/benchmarks/microbenchmarks/lib/common.dart
const String jsonStart = '================ RESULTS ================';
const String jsonEnd = '================ FORMATTED ==============';
const String jsonPrefix = ':::JSON:::';
bool jsonStarted = false;
final StringBuffer jsonBuf = StringBuffer();
final Completer<Map<String, double>> completer = Completer<Map<String, double>>();
final StreamSubscription<String> stderrSub = process.stderr
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) {
stderr.writeln('[STDERR] $line');
});
bool processWasKilledIntentionally = false;
bool resultsHaveBeenParsed = false;
final StreamSubscription<String> stdoutSub = process.stdout
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) async {
print(line);
if (line.contains(jsonStart)) {
jsonStarted = true;
return;
}
if (line.contains(jsonEnd)) {
final String jsonOutput = jsonBuf.toString();
// If we end up here and have already parsed the results, it suggests that
// we have received output from another test because our `flutter run`
// process did not terminate correctly.
// https://github.com/flutter/flutter/issues/19096#issuecomment-402756549
if (resultsHaveBeenParsed) {
throw 'Additional JSON was received after results has already been '
'processed. This suggests the `flutter run` process may have lived '
'past the end of our test and collected additional output from the '
'next test.\n\n'
'The JSON below contains all collected output, including both from '
'the original test and what followed.\n\n'
'$jsonOutput';
}
jsonStarted = false;
processWasKilledIntentionally = true;
resultsHaveBeenParsed = true;
// Sending a SIGINT/SIGTERM to the process here isn't reliable because [process] is
// the shell (flutter is a shell script) and doesn't pass the signal on.
// Sending a `q` is an instruction to quit using the console runner.
// See https://github.com/flutter/flutter/issues/19208
process.stdin.write('q');
await process.stdin.flush();
// Also send a kill signal in case the `q` above didn't work.
process.kill(ProcessSignal.sigint);
try {
completer.complete(Map<String, double>.from(json.decode(jsonOutput) as Map<String, dynamic>));
} catch (ex) {
completer.completeError('Decoding JSON failed ($ex). JSON string was: $jsonOutput');
}
return;
}
if (jsonStarted && line.contains(jsonPrefix))
jsonBuf.writeln(line.substring(line.indexOf(jsonPrefix) + jsonPrefix.length));
});
process.exitCode.then<void>((int code) async {
await Future.wait<void>(<Future<void>>[
stdoutSub.cancel(),
stderrSub.cancel(),
]);
if (!processWasKilledIntentionally && code != 0) {
completer.completeError('flutter run failed: exit code=$code');
}
});
return completer.future;
}
......@@ -3,13 +3,13 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_devicelab/framework/adb.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:flutter_devicelab/microbenchmarks.dart';
import 'package:path/path.dart' as path;
/// Creates a device lab task that runs benchmarks in
......@@ -34,14 +34,15 @@ TaskFunction createMicrobenchmarkTask() {
device.deviceId,
];
options.add(benchmarkPath);
return _startFlutter(
return startFlutter(
options: options,
canFail: false,
);
});
return _readJsonResults(flutterProcess);
return readJsonResults(flutterProcess);
}
return _run();
}
......@@ -57,99 +58,9 @@ TaskFunction createMicrobenchmarkTask() {
...await _runMicrobench('lib/language/sync_star_semantics_bench.dart'),
...await _runMicrobench('lib/foundation/all_elements_bench.dart'),
...await _runMicrobench('lib/foundation/change_notifier_bench.dart'),
};
};
return TaskResult.success(allResults, benchmarkScoreKeys: allResults.keys.toList());
return TaskResult.success(allResults,
benchmarkScoreKeys: allResults.keys.toList());
};
}
Future<Process> _startFlutter({
List<String> options = const <String>[],
bool canFail = false,
Map<String, String> environment,
}) {
final List<String> args = flutterCommandArgs('run', options);
return startProcess(path.join(flutterDirectory.path, 'bin', 'flutter'), args, environment: environment);
}
Future<Map<String, double>> _readJsonResults(Process process) {
// IMPORTANT: keep these values in sync with dev/benchmarks/microbenchmarks/lib/common.dart
const String jsonStart = '================ RESULTS ================';
const String jsonEnd = '================ FORMATTED ==============';
const String jsonPrefix = ':::JSON:::';
bool jsonStarted = false;
final StringBuffer jsonBuf = StringBuffer();
final Completer<Map<String, double>> completer = Completer<Map<String, double>>();
final StreamSubscription<String> stderrSub = process.stderr
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) {
stderr.writeln('[STDERR] $line');
});
bool processWasKilledIntentionally = false;
bool resultsHaveBeenParsed = false;
final StreamSubscription<String> stdoutSub = process.stdout
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) async {
print(line);
if (line.contains(jsonStart)) {
jsonStarted = true;
return;
}
if (line.contains(jsonEnd)) {
final String jsonOutput = jsonBuf.toString();
// If we end up here and have already parsed the results, it suggests that
// we have received output from another test because our `flutter run`
// process did not terminate correctly.
// https://github.com/flutter/flutter/issues/19096#issuecomment-402756549
if (resultsHaveBeenParsed) {
throw 'Additional JSON was received after results has already been '
'processed. This suggests the `flutter run` process may have lived '
'past the end of our test and collected additional output from the '
'next test.\n\n'
'The JSON below contains all collected output, including both from '
'the original test and what followed.\n\n'
'$jsonOutput';
}
jsonStarted = false;
processWasKilledIntentionally = true;
resultsHaveBeenParsed = true;
// Sending a SIGINT/SIGTERM to the process here isn't reliable because [process] is
// the shell (flutter is a shell script) and doesn't pass the signal on.
// Sending a `q` is an instruction to quit using the console runner.
// See https://github.com/flutter/flutter/issues/19208
process.stdin.write('q');
await process.stdin.flush();
// Also send a kill signal in case the `q` above didn't work.
process.kill(ProcessSignal.sigint);
try {
completer.complete(Map<String, double>.from(json.decode(jsonOutput) as Map<String, dynamic>));
} catch (ex) {
completer.completeError('Decoding JSON failed ($ex). JSON string was: $jsonOutput');
}
return;
}
if (jsonStarted && line.contains(jsonPrefix))
jsonBuf.writeln(line.substring(line.indexOf(jsonPrefix) + jsonPrefix.length));
});
process.exitCode.then<void>((int code) async {
await Future.wait<void>(<Future<void>>[
stdoutSub.cancel(),
stderrSub.cancel(),
]);
if (!processWasKilledIntentionally && code != 0) {
completer.completeError('flutter run failed: exit code=$code');
}
});
return completer.future;
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' show Process, Directory;
import 'package:flutter_devicelab/framework/adb.dart' as adb;
import 'package:flutter_devicelab/framework/framework.dart' show TaskFunction;
import 'package:flutter_devicelab/framework/task_result.dart' show TaskResult;
import 'package:flutter_devicelab/framework/utils.dart' as utils;
import 'package:flutter_devicelab/microbenchmarks.dart' as microbenchmarks;
import 'package:path/path.dart' as path;
TaskFunction runTask(adb.DeviceOperatingSystem operatingSystem) {
return () async {
adb.deviceOperatingSystem = operatingSystem;
final adb.Device device = await adb.devices.workingDevice;
await device.unlock();
final Directory appDir = utils.dir(path.join(utils.flutterDirectory.path,
'dev/benchmarks/platform_channels_benchmarks'));
final Process flutterProcess = await utils.inDirectory(appDir, () async {
final String flutterExe =
path.join(utils.flutterDirectory.path, 'bin', 'flutter');
final List<String> createArgs = <String>[
'create',
'--platforms',
'ios,android',
'--no-overwrite',
'-v',
'.'
];
print('\nExecuting: $flutterExe $createArgs $appDir');
utils.eval(flutterExe, createArgs);
final List<String> options = <String>[
'-v',
// --release doesn't work on iOS due to code signing issues
'--profile',
'--no-publish-port',
'-d',
device.deviceId,
];
return microbenchmarks.startFlutter(
options: options,
canFail: false,
);
});
final Map<String, double> results =
await microbenchmarks.readJsonResults(flutterProcess);
return TaskResult.success(results,
benchmarkScoreKeys: results.keys.toList());
};
}
......@@ -372,6 +372,12 @@
"task_name": "linux_picture_cache_perf__e2e_summary",
"flaky": false
},
{
"name": "Linux platform channels benchmarks",
"repo": "flutter",
"task_name": "linux_platform_channels_benchmarks",
"flaky": true
},
{
"name": "Linux platform_views_scroll_perf__timeline_summary",
"repo": "flutter",
......@@ -1152,6 +1158,12 @@
"task_name": "mac_ios_platform_channel_sample_test_swift",
"flaky": false
},
{
"name": "Mac_ios platform channels benchmarks",
"repo": "flutter",
"task_name": "mac_ios_platform_channels_benchmarks",
"flaky": true
},
{
"name": "Mac_ios platform_interaction_test_ios",
"repo": "flutter",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment