Add DevTools memory test (#55486)

diff --git a/dev/devicelab/bin/tasks/complex_layout_scroll_perf__devtools_memory.dart b/dev/devicelab/bin/tasks/complex_layout_scroll_perf__devtools_memory.dart
new file mode 100644
index 0000000..9183452
--- /dev/null
+++ b/dev/devicelab/bin/tasks/complex_layout_scroll_perf__devtools_memory.dart
@@ -0,0 +1,18 @@
+// 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 'package:flutter_devicelab/framework/adb.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:flutter_devicelab/tasks/perf_tests.dart';
+
+Future<void> main() async {
+  deviceOperatingSystem = DeviceOperatingSystem.android;
+  await task(DevToolsMemoryTest(
+    '${flutterDirectory.path}/dev/benchmarks/complex_layout',
+    'test_driver/scroll_perf.dart',
+  ).run);
+}
diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart
index ddb6a35..890bfd7 100644
--- a/dev/devicelab/lib/tasks/perf_tests.dart
+++ b/dev/devicelab/lib/tasks/perf_tests.dart
@@ -3,8 +3,9 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert' show json;
+import 'dart:convert' show LineSplitter, json, utf8;
 import 'dart:io';
+import 'dart:math' as math;
 
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
@@ -692,6 +693,129 @@
   }
 }
 
+class DevToolsMemoryTest {
+  DevToolsMemoryTest(this.project, this.driverTest);
+
+  final String project;
+  final String driverTest;
+
+  Future<TaskResult> run() {
+    return inDirectory<TaskResult>(project, () async {
+      _device = await devices.workingDevice;
+      await _device.unlock();
+      await flutter('packages', options: <String>['get']);
+
+      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,
+          '--profile',
+          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>;
+      final List<dynamic> samples = data['samples']['data'] as List<dynamic>;
+      int maxRss = 0;
+      int maxAdbTotal = 0;
+      for (final dynamic sample in samples) {
+        maxRss = math.max(maxRss, sample['rss'] as int);
+        if (sample['adb_memoryInfo'] != null) {
+          maxAdbTotal = math.max(maxAdbTotal, sample['adb_memoryInfo']['Total'] as int);
+        }
+      }
+      return TaskResult.success(
+          <String, dynamic>{'maxRss': maxRss, 'maxAdbTotal': maxAdbTotal},
+          benchmarkScoreKeys: <String>['maxRss', 'maxAdbTotal'],
+      );
+    });
+  }
+
+  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',
+        '-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.complete(match[1]);
+            _observatoryUri = match[1];
+          }
+        }, onDone: () { observatoryUri.complete(null); });
+    _forwardStream(_runProcess.stderr, 'run stderr');
+
+    _observatoryUri = await observatoryUri.future;
+  }
+
+  Future<void> _launchDevTools() async {
+    await exec('pub', <String>[
+      'global',
+      'activate',
+      'devtools',
+    ]);
+    _devToolsProcess = await startProcess(
+      'pub',
+      <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';
+}
+
 enum ReportedDurationTestFlavor {
   debug, profile, release
 }
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml
index e89c888..aacc1c5 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -285,6 +285,12 @@
     stage: devicelab
     required_agent_capabilities: ["mac/android"]
 
+  complex_layout_scroll_perf__devtools_memory:
+    description: >
+      Measures memory usage of the scroll performance test using DevTools.
+    stage: devicelab
+    required_agent_capabilities: ["linux/android"]
+
   hello_world_android__compile:
     description: >
       Measures the APK size of Hello World.