Unverified Commit a3863b65 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] support memory profiles from flutter drive (#82739)

parent 78ee9ccd
......@@ -1351,31 +1351,20 @@ class DevToolsMemoryTest {
_device = await devices.workingDevice;
await _device.unlock();
await _launchApp();
if (_observatoryUri == null) {
return TaskResult.failure('Observatory URI not found.');
}
await _launchDevTools();
await flutter(
'drive',
options: <String>[
'--use-existing-app', _observatoryUri,
'-d', _device.deviceId,
'--screenshot',
hostAgent.dumpDirectory.path,
'--profile',
'--profile-memory', _kJsonFileName,
'--no-publish-port',
'-v',
driverTest,
],
);
_devToolsProcess.kill();
await _devToolsProcess.exitCode;
_runProcess.kill();
await _runProcess.exitCode;
final Map<String, dynamic> data = json.decode(
file('$project/$_kJsonFileName').readAsStringSync(),
) as Map<String, dynamic>;
......@@ -1391,10 +1380,6 @@ class DevToolsMemoryTest {
}
}
await flutter('install', options: <String>[
'--uninstall-only',
]);
return TaskResult.success(
<String, dynamic>{'maxRss': maxRss, 'maxAdbTotal': maxAdbTotal},
benchmarkScoreKeys: <String>['maxRss', 'maxAdbTotal'],
......@@ -1402,90 +1387,7 @@ class DevToolsMemoryTest {
});
}
Future<void> _launchApp() async {
print('launching $project$driverTest on device...');
final String flutterPath = path.join(flutterDirectory.path, 'bin', 'flutter');
_runProcess = await startProcess(
flutterPath,
<String>[
'run',
'--verbose',
'--profile',
'--no-publish-port',
'-d', _device.deviceId,
driverTest,
],
);
// Listen for Observatory URI and forward stdout/stderr
final Completer<String> observatoryUri = Completer<String>();
_runProcess.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('run stdout: $line');
final RegExpMatch match = RegExp(r'An Observatory debugger and profiler on .+ is available at: ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)').firstMatch(line);
if (match != null && !observatoryUri.isCompleted) {
observatoryUri.complete(match[1]);
_observatoryUri = match[1];
}
}, onDone: () {
if (!observatoryUri.isCompleted) {
observatoryUri.complete();
}
});
_forwardStream(_runProcess.stderr, 'run stderr');
_observatoryUri = await observatoryUri.future;
}
Future<void> _launchDevTools() async {
// To mitigate https://github.com/flutter/flutter/issues/82142
await exec(pubBin, <String>[
'global',
'deactivate',
'devtools',
], canFail: true);
// The version of devtools is pinned. If we pub global activate devtools and an
// upstream devtools release breaks our CI, it will manifest on an unrelated
// commit, making it more difficult to determine the cause.
//
// Also, for release branches, all external test dependencies need to be pinned.
await exec(pubBin, <String>[
'global',
'activate',
'devtools',
'2.2.3',
// Try to debug https://github.com/flutter/flutter/issues/82142
'--verbose',
]);
_devToolsProcess = await startProcess(
pubBin,
<String>[
'global',
'run',
'devtools',
'--vm-uri', _observatoryUri,
'--profile-memory', _kJsonFileName,
],
);
_forwardStream(_devToolsProcess.stdout, 'devtools stdout');
_forwardStream(_devToolsProcess.stderr, 'devtools stderr');
}
void _forwardStream(Stream<List<int>> stream, String label) {
stream
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('$label: $line');
});
}
Device _device;
String _observatoryUri;
Process _runProcess;
Process _devToolsProcess;
static const String _kJsonFileName = 'devtools_memory.json';
}
......
......@@ -21,6 +21,7 @@ import '../dart/package_map.dart';
import '../device.dart';
import '../drive/drive_service.dart';
import '../globals.dart' as globals;
import '../resident_runner.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult, FlutterOptions;
import '../web/web_device.dart';
import 'run.dart';
......@@ -143,7 +144,10 @@ class DriveCommand extends RunCommandBase {
help: 'Attempts to write an SkSL file when the drive process is finished '
'to the provided file, overwriting it if necessary.')
..addMultiOption('test-arguments', help: 'Additional arguments to pass to the '
'Dart VM running The test script.');
'Dart VM running The test script.')
..addOption('profile-memory', help: 'Launch devtools and profile application memory, writing '
'The output data to the file path provided to this argument as JSON.',
valueHelp: 'profile_memory.json');
}
// `pub` must always be run due to the test script running from source,
......@@ -215,6 +219,7 @@ class DriveCommand extends RunCommandBase {
logger: _logger,
processUtils: globals.processUtils,
dartSdkPath: globals.artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
devtoolsLauncher: DevtoolsLauncher.instance,
);
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
_fileSystem.file('.packages'),
......@@ -271,6 +276,7 @@ class DriveCommand extends RunCommandBase {
? int.tryParse(stringArg('driver-port'))
: null,
androidEmulator: boolArg('android-emulator'),
profileMemory: stringArg('profile-memory'),
);
if (testResult != 0 && screenshot != null) {
await takeScreenshot(device, screenshot, _fileSystem, _logger, _fsUtils);
......
......@@ -41,6 +41,7 @@ class DevtoolsServerLauncher extends DevtoolsLauncher {
final Platform _platform;
final PersistentToolState _persistentToolState;
final io.HttpClient _httpClient;
final Completer<void> _processStartCompleter = Completer<void>();
io.Process _devToolsProcess;
......@@ -49,7 +50,10 @@ class DevtoolsServerLauncher extends DevtoolsLauncher {
static const String _pubHostedUrlKey = 'PUB_HOSTED_URL';
@override
Future<void> launch(Uri vmServiceUri) async {
Future<void> get processStart => _processStartCompleter.future;
@override
Future<void> launch(Uri vmServiceUri, {List<String> additionalArguments}) async {
// Place this entire method in a try/catch that swallows exceptions because
// this method is guaranteed not to return a Future that throws.
try {
......@@ -116,7 +120,9 @@ class DevtoolsServerLauncher extends DevtoolsLauncher {
'devtools',
'--no-launch-browser',
if (vmServiceUri != null) '--vm-uri=$vmServiceUri',
...?additionalArguments,
]);
_processStartCompleter.complete();
final Completer<Uri> completer = Completer<Uri>();
_devToolsProcess.stdout
.transform(utf8.decoder)
......
......@@ -4,6 +4,8 @@
// @dart = 2.8
import 'dart:async';
import 'package:dds/dds.dart' as dds;
import 'package:file/file.dart';
import 'package:meta/meta.dart';
......@@ -16,6 +18,7 @@ import '../base/logger.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../device.dart';
import '../resident_runner.dart';
import '../sksl_writer.dart';
import '../vmservice.dart';
import 'web_driver_service.dart';
......@@ -26,15 +29,18 @@ class FlutterDriverFactory {
@required Logger logger,
@required ProcessUtils processUtils,
@required String dartSdkPath,
@required DevtoolsLauncher devtoolsLauncher,
}) : _applicationPackageFactory = applicationPackageFactory,
_logger = logger,
_processUtils = processUtils,
_dartSdkPath = dartSdkPath;
_dartSdkPath = dartSdkPath,
_devtoolsLauncher = devtoolsLauncher;
final ApplicationPackageFactory _applicationPackageFactory;
final Logger _logger;
final ProcessUtils _processUtils;
final String _dartSdkPath;
final DevtoolsLauncher _devtoolsLauncher;
/// Create a driver service for running `flutter drive`.
DriverService createDriverService(bool web) {
......@@ -49,6 +55,7 @@ class FlutterDriverFactory {
processUtils: _processUtils,
dartSdkPath: _dartSdkPath,
applicationPackageFactory: _applicationPackageFactory,
devtoolsLauncher: _devtoolsLauncher,
);
}
}
......@@ -78,6 +85,9 @@ abstract class DriverService {
/// Start the test file with the provided [arguments] and [environment], returning
/// the test process exit code.
///
/// if [profileMemory] is provided, it will be treated as a file path to write a
/// devtools memory profile.
Future<int> startTest(
String testFile,
List<String> arguments,
......@@ -89,6 +99,7 @@ abstract class DriverService {
bool androidEmulator,
int driverPort,
List<String> browserDimension,
String profileMemory,
});
/// Stop the running application and uninstall it from the device.
......@@ -110,12 +121,14 @@ class FlutterDriverService extends DriverService {
@required Logger logger,
@required ProcessUtils processUtils,
@required String dartSdkPath,
@required DevtoolsLauncher devtoolsLauncher,
@visibleForTesting VMServiceConnector vmServiceConnector = connectToVmService,
}) : _applicationPackageFactory = applicationPackageFactory,
_logger = logger,
_processUtils = processUtils,
_dartSdkPath = dartSdkPath,
_vmServiceConnector = vmServiceConnector;
_vmServiceConnector = vmServiceConnector,
_devtoolsLauncher = devtoolsLauncher;
static const int _kLaunchAttempts = 3;
......@@ -124,6 +137,7 @@ class FlutterDriverService extends DriverService {
final ProcessUtils _processUtils;
final String _dartSdkPath;
final VMServiceConnector _vmServiceConnector;
final DevtoolsLauncher _devtoolsLauncher;
Device _device;
ApplicationPackage _applicationPackage;
......@@ -242,14 +256,30 @@ class FlutterDriverService extends DriverService {
bool androidEmulator,
int driverPort,
List<String> browserDimension,
String profileMemory,
}) async {
return _processUtils.stream(<String>[
_dartSdkPath,
...<String>[...arguments, testFile, '-rexpanded'],
], environment: <String, String>{
'VM_SERVICE_URL': _vmServiceUri,
...environment,
});
if (profileMemory != null) {
unawaited(_devtoolsLauncher.launch(
Uri.parse(_vmServiceUri),
additionalArguments: <String>['--profile-memory=$profileMemory'],
));
// When profiling memory the original launch future will never complete.
await _devtoolsLauncher.processStart;
}
try {
final int result = await _processUtils.stream(<String>[
_dartSdkPath,
...<String>[...arguments, testFile, '-rexpanded'],
], environment: <String, String>{
'VM_SERVICE_URL': _vmServiceUri,
...environment,
});
return result;
} finally {
if (profileMemory != null) {
await _devtoolsLauncher.close();
}
}
}
@override
......
......@@ -97,6 +97,7 @@ class WebDriverService extends DriverService {
bool androidEmulator,
int driverPort,
List<String> browserDimension,
String profileMemory,
}) async {
async_io.WebDriver webDriver;
final Browser browser = _browserNameToEnum(browserName);
......
......@@ -1802,13 +1802,21 @@ abstract class DevtoolsLauncher {
/// Launch a Dart DevTools process, optionally targeting a specific VM Service
/// URI if [vmServiceUri] is non-null.
///
/// [additionalArguments] may be optionally specified and are passed directly
/// to the devtools run command.
///
/// This method must return a future that is guaranteed not to fail, because it
/// will be used in unawaited contexts.
@visibleForTesting
Future<void> launch(Uri vmServiceUri);
Future<void> launch(Uri vmServiceUri, {List<String> additionalArguments});
Future<void> close();
/// When measuring devtools memory via addtional arguments, the launch process
/// will technically never complete.
///
/// Us this as an indicator that the process has started.
Future<void> processStart;
/// Returns a future that completes when the DevTools server is ready.
///
/// Completes when [devToolsUrl] is set. That can be set either directly, or
......
......@@ -275,6 +275,45 @@ void main() {
await launcher.serve();
});
testWithoutContext('DevtoolsLauncher can launch devtools with a memory profile', () async {
persistentToolState.lastDevToolsActivation = DateTime.now();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'pub',
'global',
'list',
],
stdout: 'devtools 0.9.6',
),
const FakeCommand(
command: <String>[
'pub',
'global',
'run',
'devtools',
'--no-launch-browser',
'--vm-uri=localhost:8181/abcdefg',
'--profile-memory=foo'
],
stdout: 'Serving DevTools at http://127.0.0.1:9100\n',
),
]);
final DevtoolsLauncher launcher = DevtoolsServerLauncher(
pubExecutable: 'pub',
logger: logger,
platform: platform,
persistentToolState: persistentToolState,
httpClient: FakeHttpClient.any(),
processManager: processManager,
);
await launcher.launch(Uri.parse('localhost:8181/abcdefg'), additionalArguments: <String>['--profile-memory=foo']);
expect(launcher.processStart, completes);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('DevtoolsLauncher prints error if exception is thrown during activate', () async {
final DevtoolsLauncher launcher = DevtoolsServerLauncher(
pubExecutable: 'pub',
......
......@@ -4,6 +4,8 @@
// @dart = 2.8
import 'dart:async';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
......@@ -14,6 +16,7 @@ import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/drive/drive_service.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:meta/meta.dart';
......@@ -182,6 +185,39 @@ void main() {
expect(testResult, 23);
});
testWithoutContext('Connects to device VM Service and runs test application with devtools memory profile', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
getVM,
]);
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['dart', '--enable-experiment=non-nullable', 'foo.test', '-rexpanded'],
exitCode: 23,
environment: <String, String>{
'FOO': 'BAR',
'VM_SERVICE_URL': 'http://127.0.0.1:1234/' // dds forwarded URI
},
),
]);
final FakeDevtoolsLauncher launcher = FakeDevtoolsLauncher();
final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService, devtoolsLauncher: launcher);
final Device device = FakeDevice(LaunchResult.succeeded(
observatoryUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
));
await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true);
final int testResult = await driverService.startTest(
'foo.test',
<String>['--enable-experiment=non-nullable'],
<String, String>{'FOO': 'BAR'},
PackageConfig(<Package>[Package('test', Uri.base)]),
profileMemory: 'devtools_memory.json',
);
expect(launcher.closed, true);
expect(testResult, 23);
});
testWithoutContext('Uses dart to execute the test if there is no package:test dependency', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
getVM,
......@@ -424,6 +460,7 @@ FlutterDriverService setUpDriverService({
Logger logger,
ProcessManager processManager,
FlutterVmService vmService,
DevtoolsLauncher devtoolsLauncher,
}) {
logger ??= BufferLogger.test();
return FlutterDriverService(
......@@ -434,6 +471,7 @@ FlutterDriverService setUpDriverService({
processManager: processManager ?? FakeProcessManager.any(),
),
dartSdkPath: 'dart',
devtoolsLauncher: devtoolsLauncher ?? FakeDevtoolsLauncher(),
vmServiceConnector: (Uri httpUri, {
ReloadSources reloadSources,
Restart restart,
......@@ -557,3 +595,22 @@ class FakeDartDevelopmentService extends Fake implements DartDevelopmentService
disposed = true;
}
}
class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
bool closed = false;
final Completer<void> _processStarted = Completer<void>();
@override
Future<void> launch(Uri vmServiceUri, {List<String> additionalArguments}) {
_processStarted.complete();
return Completer<void>().future;
}
@override
Future<void> get processStart => _processStarted.future;
@override
Future<void> close() async {
closed = true;
}
}
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