From 9391e480b2cbf0905dc2630f383e562e1f05ef67 Mon Sep 17 00:00:00 2001
From: Jonah Williams <jonahwilliams@google.com>
Date: Thu, 19 Mar 2020 10:16:20 -0700
Subject: [PATCH] Revert "[flutter_tools] refactor GenSnapshot and AotBuilder
 (#52091)" (#52893)

This reverts commit f65421aaca381f55bee97dd0e0292b553aeaffb7.
---
 packages/flutter_tools/lib/src/aot.dart       |  171 ++-
 .../flutter_tools/lib/src/base/build.dart     |  181 ++-
 .../flutter_tools/lib/src/base/logger.dart    |    6 +-
 .../lib/src/build_system/targets/android.dart |    9 +-
 .../lib/src/build_system/targets/dart.dart    |   13 +-
 .../lib/src/build_system/targets/ios.dart     |   12 +-
 .../lib/src/build_system/targets/macos.dart   |   10 +-
 .../lib/src/commands/build_aot.dart           |    6 +
 .../lib/src/commands/build_ios_framework.dart |    1 +
 .../flutter_tools/lib/src/context_runner.dart |    2 +
 .../flutter_tools/lib/src/ios/xcodeproj.dart  |    4 +-
 .../test/general.shard/base/build_test.dart   | 1184 ++++++++++-------
 .../build_system/targets/macos_test.dart      |   32 +
 13 files changed, 960 insertions(+), 671 deletions(-)

diff --git a/packages/flutter_tools/lib/src/aot.dart b/packages/flutter_tools/lib/src/aot.dart
index 83ee0e935b..77796f9b6f 100644
--- a/packages/flutter_tools/lib/src/aot.dart
+++ b/packages/flutter_tools/lib/src/aot.dart
@@ -6,18 +6,15 @@ import 'dart:async';
 
 import 'package:meta/meta.dart';
 
+import 'base/build.dart';
 import 'base/common.dart';
+import 'base/io.dart';
 import 'base/logger.dart';
+import 'base/process.dart';
 import 'build_info.dart';
-import 'build_system/build_system.dart';
-import 'build_system/targets/dart.dart';
-import 'build_system/targets/icon_tree_shaker.dart';
-import 'build_system/targets/ios.dart';
-import 'cache.dart';
-import 'convert.dart';
+import 'dart/package_map.dart';
 import 'globals.dart' as globals;
 import 'ios/bitcode.dart';
-import 'project.dart';
 
 /// Builds AOT snapshots given a platform, build mode and a path to a Dart
 /// library.
@@ -29,41 +26,18 @@ class AotBuilder {
     @required String mainDartFile,
     bool bitcode = kBitcodeEnabledDefault,
     bool quiet = true,
+    bool reportTimings = false,
     Iterable<DarwinArch> iosBuildArchs = defaultIOSArchs,
   }) async {
     if (platform == null) {
       throwToolExit('No AOT build platform specified');
     }
-    Target target;
-    bool expectSo = false;
-    switch (platform) {
-      case TargetPlatform.android:
-      case TargetPlatform.darwin_x64:
-      case TargetPlatform.linux_x64:
-      case TargetPlatform.windows_x64:
-      case TargetPlatform.tester:
-      case TargetPlatform.web_javascript:
-      case TargetPlatform.android_x86:
-        throwToolExit('$platform is not supported in AOT.');
-        break;
-      case TargetPlatform.fuchsia_arm64:
-      case TargetPlatform.fuchsia_x64:
-        throwToolExit(
-          "To build release for fuchsia, use 'flutter build fuchsia --release'"
-        );
-        break;
-      case TargetPlatform.ios:
-        target = buildInfo.isRelease
-          ? const AotAssemblyRelease()
-          : const AotAssemblyProfile();
-        break;
-      case TargetPlatform.android_arm:
-      case TargetPlatform.android_arm64:
-      case TargetPlatform.android_x64:
-        expectSo = true;
-        target = buildInfo.isRelease
-          ? const AotElfRelease()
-          : const AotElfProfile();
+
+    if (bitcode) {
+      if (platform != TargetPlatform.ios) {
+        throwToolExit('Bitcode is only supported on iOS (TargetPlatform is $platform).');
+      }
+      await validateBitcode(buildInfo.mode, platform);
     }
 
     Status status;
@@ -74,45 +48,102 @@ class AotBuilder {
         timeout: timeoutConfiguration.slowOperation,
       );
     }
+    try {
+      final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: reportTimings);
 
-    final Environment environment = Environment(
-      projectDir: globals.fs.currentDirectory,
-      outputDir: globals.fs.directory(outputPath),
-      buildDir: FlutterProject.current().dartTool.childDirectory('flutter_build'),
-      cacheDir: null,
-      flutterRootDir: globals.fs.directory(Cache.flutterRoot),
-      defines: <String, String>{
-        kTargetFile: mainDartFile ?? globals.fs.path.join('lib', 'main.dart'),
-        kBuildMode: getNameForBuildMode(buildInfo.mode),
-        kTargetPlatform: getNameForTargetPlatform(platform),
-        kIconTreeShakerFlag: buildInfo.treeShakeIcons.toString(),
-        kDartDefines: jsonEncode(buildInfo.dartDefines),
-        if (buildInfo?.extraGenSnapshotOptions?.isNotEmpty ?? false)
-          kExtraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions.join(','),
-        if (buildInfo?.extraFrontEndOptions?.isNotEmpty ?? false)
-          kExtraFrontEndOptions: buildInfo.extraFrontEndOptions.join(','),
-        if (platform == TargetPlatform.ios)
-          kIosArchs: iosBuildArchs.map(getNameForDarwinArch).join(' ')
+      // Compile to kernel.
+      final String kernelOut = await snapshotter.compileKernel(
+        platform: platform,
+        buildMode: buildInfo.mode,
+        mainPath: mainDartFile,
+        packagesPath: PackageMap.globalPackagesPath,
+        trackWidgetCreation: buildInfo.trackWidgetCreation,
+        outputPath: outputPath,
+        extraFrontEndOptions: buildInfo.extraFrontEndOptions,
+        dartDefines: buildInfo.dartDefines
+      );
+      if (kernelOut == null) {
+        throwToolExit('Compiler terminated unexpectedly.');
+        return;
       }
-    );
-    final BuildResult result = await buildSystem.build(target, environment);
-    status?.stop();
 
-    if (!result.success) {
-      for (final ExceptionMeasurement measurement in result.exceptions.values) {
-        globals.printError(measurement.exception.toString());
+      // Build AOT snapshot.
+      if (platform == TargetPlatform.ios) {
+        // Determine which iOS architectures to build for.
+        final Map<DarwinArch, String> iosBuilds = <DarwinArch, String>{};
+        for (final DarwinArch arch in iosBuildArchs) {
+          iosBuilds[arch] = globals.fs.path.join(outputPath, getNameForDarwinArch(arch));
+        }
+
+        // Generate AOT snapshot and compile to arch-specific App.framework.
+        final Map<DarwinArch, Future<int>> exitCodes = <DarwinArch, Future<int>>{};
+        iosBuilds.forEach((DarwinArch iosArch, String outputPath) {
+          exitCodes[iosArch] = snapshotter.build(
+            platform: platform,
+            darwinArch: iosArch,
+            buildMode: buildInfo.mode,
+            mainPath: kernelOut,
+            packagesPath: PackageMap.globalPackagesPath,
+            outputPath: outputPath,
+            extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions,
+            bitcode: bitcode,
+            quiet: quiet,
+            splitDebugInfo: null,
+            dartObfuscation: false,
+          ).then<int>((int buildExitCode) {
+            return buildExitCode;
+          });
+        });
+
+        // Merge arch-specific App.frameworks into a multi-arch App.framework.
+        if ((await Future.wait<int>(exitCodes.values)).every((int buildExitCode) => buildExitCode == 0)) {
+          final Iterable<String> dylibs = iosBuilds.values.map<String>(
+              (String outputDir) => globals.fs.path.join(outputDir, 'App.framework', 'App'));
+          globals.fs.directory(globals.fs.path.join(outputPath, 'App.framework')).createSync();
+          await processUtils.run(
+            <String>[
+              'lipo',
+              ...dylibs,
+              '-create',
+              '-output', globals.fs.path.join(outputPath, 'App.framework', 'App'),
+            ],
+            throwOnError: true,
+          );
+        } else {
+          status?.cancel();
+          exitCodes.forEach((DarwinArch iosArch, Future<int> exitCodeFuture) async {
+            final int buildExitCode = await exitCodeFuture;
+            globals.printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode');
+          });
+        }
+      } else {
+        // Android AOT snapshot.
+        final int snapshotExitCode = await snapshotter.build(
+          platform: platform,
+          buildMode: buildInfo.mode,
+          mainPath: kernelOut,
+          packagesPath: PackageMap.globalPackagesPath,
+          outputPath: outputPath,
+          extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions,
+          bitcode: false,
+          splitDebugInfo: null,
+          dartObfuscation: false,
+        );
+        if (snapshotExitCode != 0) {
+          status?.cancel();
+          throwToolExit('Snapshotting exited with non-zero exit code: $snapshotExitCode');
+        }
       }
-      throwToolExit('The aot build failed.');
+    } on ProcessException catch (error) {
+      // Catch the String exceptions thrown from the `runSync` methods below.
+      status?.cancel();
+      globals.printError(error.toString());
+      return;
     }
+    status?.stop();
 
-    if (expectSo) {
-      environment.buildDir.childFile('app.so')
-        .copySync(globals.fs.path.join(outputPath, 'app.so'));
-    } else {
-      globals.fs.directory(globals.fs.path.join(outputPath, 'App.framework'))
-        .createSync(recursive: true);
-      environment.buildDir.childDirectory('App.framework').childFile('App')
-        .copySync(globals.fs.path.join(outputPath, 'App.framework', 'App'));
+    if (outputPath == null) {
+      throwToolExit(null);
     }
 
     final String builtMessage = 'Built to $outputPath${globals.fs.path.separator}.';
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index 235765a1e3..a1f05391fd 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -3,17 +3,23 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+
 import 'package:meta/meta.dart';
-import 'package:process/process.dart';
 
 import '../artifacts.dart';
 import '../build_info.dart';
+import '../bundle.dart';
+import '../compile.dart';
+import '../globals.dart' as globals;
 import '../macos/xcode.dart';
+import '../project.dart';
 
+import 'context.dart';
 import 'file_system.dart';
-import 'logger.dart';
 import 'process.dart';
 
+GenSnapshot get genSnapshot => context.get<GenSnapshot>();
+
 /// A snapshot build configuration.
 class SnapshotType {
   SnapshotType(this.platform, this.mode)
@@ -28,18 +34,10 @@ class SnapshotType {
 
 /// Interface to the gen_snapshot command-line tool.
 class GenSnapshot {
-  GenSnapshot({
-    @required Artifacts artifacts,
-    @required ProcessManager processManager,
-    @required Logger logger,
-  }) : _artifacts = artifacts,
-       _processUtils = ProcessUtils(logger: logger, processManager: processManager);
-
-  final Artifacts _artifacts;
-  final ProcessUtils _processUtils;
-
-  String getSnapshotterPath(SnapshotType snapshotType) {
-    return _artifacts.getArtifactPath(
+  const GenSnapshot();
+
+  static String getSnapshotterPath(SnapshotType snapshotType) {
+    return globals.artifacts.getArtifactPath(
         Artifact.genSnapshot, platform: snapshotType.platform, mode: snapshotType.mode);
   }
 
@@ -53,7 +51,6 @@ class GenSnapshot {
     'Warning: This VM has been configured to obfuscate symbol information which violates the Dart standard.',
     '         See dartbug.com/30524 for more information.',
   };
-
   Future<int> run({
     @required SnapshotType snapshotType,
     DarwinArch darwinArch,
@@ -71,7 +68,7 @@ class GenSnapshot {
       snapshotterPath += '_' + getNameForDarwinArch(darwinArch);
     }
 
-    return _processUtils.stream(
+    return processUtils.stream(
       <String>[snapshotterPath, ...args],
       mapFunction: (String line) =>  kIgnoredWarnings.contains(line) ? null : line,
     );
@@ -79,26 +76,7 @@ class GenSnapshot {
 }
 
 class AOTSnapshotter {
-  AOTSnapshotter({
-    this.reportTimings = false,
-    @required Logger logger,
-    @required FileSystem fileSystem,
-    @required Xcode xcode,
-    @required ProcessManager processManager,
-    @required Artifacts artifacts,
-  }) : _logger = logger,
-      _fileSystem = fileSystem,
-      _xcode = xcode,
-      _genSnapshot = GenSnapshot(
-        artifacts: artifacts,
-        processManager: processManager,
-        logger: logger,
-      );
-
-  final Logger _logger;
-  final FileSystem _fileSystem;
-  final Xcode _xcode;
-  final GenSnapshot _genSnapshot;
+  AOTSnapshotter({this.reportTimings = false});
 
   /// If true then AOTSnapshotter would report timings for individual building
   /// steps (Dart front-end parsing and snapshot generation) in a stable
@@ -119,43 +97,40 @@ class AOTSnapshotter {
     @required bool dartObfuscation,
     bool quiet = false,
   }) async {
-    // TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
-    assert(platform != TargetPlatform.ios || darwinArch != null);
     if (bitcode && platform != TargetPlatform.ios) {
-      _logger.printError('Bitcode is only supported for iOS.');
+      globals.printError('Bitcode is only supported for iOS.');
       return 1;
     }
 
     if (!_isValidAotPlatform(platform, buildMode)) {
-      _logger.printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
+      globals.printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
       return 1;
     }
+    // TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
+    assert(platform != TargetPlatform.ios || darwinArch != null);
 
-    final Directory outputDir = _fileSystem.directory(outputPath);
+    final Directory outputDir = globals.fs.directory(outputPath);
     outputDir.createSync(recursive: true);
 
     final List<String> genSnapshotArgs = <String>[
       '--deterministic',
     ];
     if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
-      _logger.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
+      globals.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
       genSnapshotArgs.addAll(extraGenSnapshotOptions);
     }
 
-    final String assembly = _fileSystem.path.join(outputDir.path, 'snapshot_assembly.S');
+    final String assembly = globals.fs.path.join(outputDir.path, 'snapshot_assembly.S');
     if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin_x64) {
-      genSnapshotArgs.addAll(<String>[
-        '--snapshot_kind=app-aot-assembly',
-        '--assembly=$assembly',
-        '--strip'
-      ]);
+      // Assembly AOT snapshot.
+      genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');
+      genSnapshotArgs.add('--assembly=$assembly');
+      genSnapshotArgs.add('--strip');
     } else {
-      final String aotSharedLibrary = _fileSystem.path.join(outputDir.path, 'app.so');
-      genSnapshotArgs.addAll(<String>[
-        '--snapshot_kind=app-aot-elf',
-        '--elf=$aotSharedLibrary',
-        '--strip'
-      ]);
+      final String aotSharedLibrary = globals.fs.path.join(outputDir.path, 'app.so');
+      genSnapshotArgs.add('--snapshot_kind=app-aot-elf');
+      genSnapshotArgs.add('--elf=$aotSharedLibrary');
+      genSnapshotArgs.add('--strip');
     }
 
     if (platform == TargetPlatform.android_arm || darwinArch == DarwinArch.armv7) {
@@ -175,7 +150,7 @@ class AOTSnapshotter {
     final String debugFilename = 'app.$archName.symbols';
     final bool shouldSplitDebugInfo = splitDebugInfo?.isNotEmpty ?? false;
     if (shouldSplitDebugInfo) {
-      _fileSystem.directory(splitDebugInfo)
+      globals.fs.directory(splitDebugInfo)
         .createSync(recursive: true);
     }
 
@@ -186,7 +161,7 @@ class AOTSnapshotter {
       '--lazy-async-stacks',
       if (shouldSplitDebugInfo) ...<String>[
         '--dwarf-stack-traces',
-        '--save-debugging-info=${_fileSystem.path.join(splitDebugInfo, debugFilename)}'
+        '--save-debugging-info=${globals.fs.path.join(splitDebugInfo, debugFilename)}'
       ],
       if (dartObfuscation)
         '--obfuscate',
@@ -195,13 +170,15 @@ class AOTSnapshotter {
     genSnapshotArgs.add(mainPath);
 
     final SnapshotType snapshotType = SnapshotType(platform, buildMode);
-    final int genSnapshotExitCode = await _genSnapshot.run(
+    final int genSnapshotExitCode =
+      await _timedStep('snapshot(CompileTime)', 'aot-snapshot',
+        () => genSnapshot.run(
       snapshotType: snapshotType,
       additionalArgs: genSnapshotArgs,
       darwinArch: darwinArch,
-    );
+    ));
     if (genSnapshotExitCode != 0) {
-      _logger.printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
+      globals.printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
       return genSnapshotExitCode;
     }
 
@@ -235,7 +212,7 @@ class AOTSnapshotter {
   }) async {
     final String targetArch = getNameForDarwinArch(appleArch);
     if (!quiet) {
-      _logger.printStatus('Building App.framework for $targetArch...');
+      globals.printStatus('Building App.framework for $targetArch...');
     }
 
     final List<String> commonBuildOptions = <String>[
@@ -245,15 +222,15 @@ class AOTSnapshotter {
     ];
 
     const String embedBitcodeArg = '-fembed-bitcode';
-    final String assemblyO = _fileSystem.path.join(outputPath, 'snapshot_assembly.o');
+    final String assemblyO = globals.fs.path.join(outputPath, 'snapshot_assembly.o');
     List<String> isysrootArgs;
     if (isIOS) {
-      final String iPhoneSDKLocation = await _xcode.sdkLocation(SdkType.iPhone);
+      final String iPhoneSDKLocation = await globals.xcode.sdkLocation(SdkType.iPhone);
       if (iPhoneSDKLocation != null) {
         isysrootArgs = <String>['-isysroot', iPhoneSDKLocation];
       }
     }
-    final RunResult compileResult = await _xcode.cc(<String>[
+    final RunResult compileResult = await globals.xcode.cc(<String>[
       '-arch', targetArch,
       if (isysrootArgs != null) ...isysrootArgs,
       if (bitcode) embedBitcodeArg,
@@ -263,13 +240,13 @@ class AOTSnapshotter {
       assemblyO,
     ]);
     if (compileResult.exitCode != 0) {
-      _logger.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
+      globals.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
       return compileResult;
     }
 
-    final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework');
-    _fileSystem.directory(frameworkDir).createSync(recursive: true);
-    final String appLib = _fileSystem.path.join(frameworkDir, 'App');
+    final String frameworkDir = globals.fs.path.join(outputPath, 'App.framework');
+    globals.fs.directory(frameworkDir).createSync(recursive: true);
+    final String appLib = globals.fs.path.join(frameworkDir, 'App');
     final List<String> linkArgs = <String>[
       ...commonBuildOptions,
       '-dynamiclib',
@@ -281,13 +258,64 @@ class AOTSnapshotter {
       '-o', appLib,
       assemblyO,
     ];
-    final RunResult linkResult = await _xcode.clang(linkArgs);
+    final RunResult linkResult = await globals.xcode.clang(linkArgs);
     if (linkResult.exitCode != 0) {
-      _logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
+      globals.printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
     }
     return linkResult;
   }
 
+  /// Compiles a Dart file to kernel.
+  ///
+  /// Returns the output kernel file path, or null on failure.
+  Future<String> compileKernel({
+    @required TargetPlatform platform,
+    @required BuildMode buildMode,
+    @required String mainPath,
+    @required String packagesPath,
+    @required String outputPath,
+    @required bool trackWidgetCreation,
+    @required List<String> dartDefines,
+    List<String> extraFrontEndOptions = const <String>[],
+  }) async {
+    final FlutterProject flutterProject = FlutterProject.current();
+    final Directory outputDir = globals.fs.directory(outputPath);
+    outputDir.createSync(recursive: true);
+
+    globals.printTrace('Compiling Dart to kernel: $mainPath');
+
+    if ((extraFrontEndOptions != null) && extraFrontEndOptions.isNotEmpty) {
+      globals.printTrace('Extra front-end options: $extraFrontEndOptions');
+    }
+
+    final String depfilePath = globals.fs.path.join(outputPath, 'kernel_compile.d');
+    final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(flutterProject);
+    final CompilerOutput compilerOutput =
+      await _timedStep('frontend(CompileTime)', 'aot-kernel',
+        () => kernelCompiler.compile(
+      sdkRoot: globals.artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
+      mainPath: mainPath,
+      packagesPath: packagesPath,
+      outputFilePath: getKernelPathForTransformerOptions(
+        globals.fs.path.join(outputPath, 'app.dill'),
+        trackWidgetCreation: trackWidgetCreation,
+      ),
+      depFilePath: depfilePath,
+      extraFrontEndOptions: extraFrontEndOptions,
+      linkPlatformKernelIn: true,
+      aot: true,
+      buildMode: buildMode,
+      trackWidgetCreation: trackWidgetCreation,
+      dartDefines: dartDefines,
+    ));
+
+    // Write path to frontend_server, since things need to be re-generated when that changes.
+    final String frontendPath = globals.artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk);
+    globals.fs.directory(outputPath).childFile('frontend_server.d').writeAsStringSync('frontend_server.d: $frontendPath\n');
+
+    return compilerOutput?.outputFilename;
+  }
+
   bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
     if (buildMode == BuildMode.debug) {
       return false;
@@ -300,4 +328,19 @@ class AOTSnapshotter {
       TargetPlatform.darwin_x64,
     ].contains(platform);
   }
+
+  /// This method is used to measure duration of an action and emit it into
+  /// verbose output from flutter_tool for other tools (e.g. benchmark runner)
+  /// to find.
+  /// Important: external performance tracking tools expect format of this
+  /// output to be stable.
+  Future<T> _timedStep<T>(String marker, String analyticsVar, FutureOr<T> Function() action) async {
+    final Stopwatch sw = Stopwatch()..start();
+    final T value = await action();
+    if (reportTimings) {
+      globals.printStatus('$marker: ${sw.elapsedMilliseconds} ms.');
+    }
+    globals.flutterUsage.sendTiming('build', analyticsVar, Duration(milliseconds: sw.elapsedMilliseconds));
+    return value;
+  }
 }
diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart
index d946e22c8c..0d9283311f 100644
--- a/packages/flutter_tools/lib/src/base/logger.dart
+++ b/packages/flutter_tools/lib/src/base/logger.dart
@@ -697,7 +697,7 @@ abstract class Status {
     @required Duration timeout,
     @required TimeoutConfiguration timeoutConfiguration,
     @required Stopwatch stopwatch,
-    @required Terminal terminal,
+    @required AnsiTerminal terminal,
     VoidCallback onFinish,
     SlowWarningCallback slowWarningCallback,
   }) {
@@ -877,7 +877,7 @@ class AnsiSpinner extends Status {
     @required Duration timeout,
     @required TimeoutConfiguration timeoutConfiguration,
     @required Stopwatch stopwatch,
-    @required Terminal terminal,
+    @required AnsiTerminal terminal,
     VoidCallback onFinish,
     this.slowWarningCallback,
     Stdio stdio,
@@ -893,7 +893,7 @@ class AnsiSpinner extends Status {
   final String _backspaceChar = '\b';
   final String _clearChar = ' ';
   final Stdio _stdio;
-  final Terminal _terminal;
+  final AnsiTerminal _terminal;
 
   bool timedOut = false;
 
diff --git a/packages/flutter_tools/lib/src/build_system/targets/android.dart b/packages/flutter_tools/lib/src/build_system/targets/android.dart
index d832c50f58..ed677b2589 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/android.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/android.dart
@@ -210,14 +210,7 @@ class AndroidAot extends AotElfBase {
 
   @override
   Future<void> build(Environment environment) async {
-    final AOTSnapshotter snapshotter = AOTSnapshotter(
-      reportTimings: false,
-      fileSystem: globals.fs,
-      logger: globals.logger,
-      xcode: globals.xcode,
-      processManager: globals.processManager,
-      artifacts: globals.artifacts,
-    );
+    final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
     final Directory output = environment.buildDir.childDirectory(_androidAbiName);
     final String splitDebugInfo = environment.defines[kSplitDebugInfo];
     if (environment.defines[kBuildMode] == null) {
diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
index a67e834293..afa08b77ff 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -204,7 +204,9 @@ class KernelSnapshot extends Target {
     final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
 
     // This configuration is all optional.
-    final List<String> extraFrontEndOptions = environment.defines[kExtraFrontEndOptions]?.split(',');
+    final List<String> extraFrontEndOptions = <String>[
+      ...?environment.defines[kExtraFrontEndOptions]?.split(',')
+    ];
     final List<String> fileSystemRoots = environment.defines[kFileSystemRoots]?.split(',');
     final String fileSystemScheme = environment.defines[kFileSystemScheme];
 
@@ -259,14 +261,7 @@ abstract class AotElfBase extends Target {
 
   @override
   Future<void> build(Environment environment) async {
-    final AOTSnapshotter snapshotter = AOTSnapshotter(
-      reportTimings: false,
-      fileSystem: globals.fs,
-      logger: globals.logger,
-      xcode: globals.xcode,
-      processManager: globals.processManager,
-      artifacts: globals.artifacts,
-    );
+    final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
     final String outputPath = environment.buildDir.path;
     if (environment.defines[kBuildMode] == null) {
       throw MissingDefineException(kBuildMode, 'aot_elf');
diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
index 20e685ab0d..990716cf8a 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
@@ -28,14 +28,7 @@ abstract class AotAssemblyBase extends Target {
 
   @override
   Future<void> build(Environment environment) async {
-    final AOTSnapshotter snapshotter = AOTSnapshotter(
-      reportTimings: false,
-      fileSystem: globals.fs,
-      logger: globals.logger,
-      xcode: globals.xcode,
-      artifacts: globals.artifacts,
-      processManager: globals.processManager,
-    );
+    final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
     final String buildOutputPath = environment.buildDir.path;
     if (environment.defines[kBuildMode] == null) {
       throw MissingDefineException(kBuildMode, 'aot_assembly');
@@ -43,8 +36,6 @@ abstract class AotAssemblyBase extends Target {
     if (environment.defines[kTargetPlatform] == null) {
       throw MissingDefineException(kTargetPlatform, 'aot_assembly');
     }
-    final List<String> extraGenSnapshotOptions = environment
-      .defines[kExtraGenSnapshotOptions]?.split(',') ?? const <String>[];
     final bool bitcode = environment.defines[kBitcodeFlag] == 'true';
     final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
     final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
@@ -74,7 +65,6 @@ abstract class AotAssemblyBase extends Target {
         quiet: true,
         splitDebugInfo: splitDebugInfo,
         dartObfuscation: dartObfuscation,
-        extraGenSnapshotOptions: extraGenSnapshotOptions,
       ));
     }
     final List<int> results = await Future.wait(pending);
diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
index 5104d99901..a4d5c9dfe3 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart
@@ -202,15 +202,7 @@ class CompileMacOSFramework extends Target {
     }
     final String splitDebugInfo = environment.defines[kSplitDebugInfo];
     final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true';
-    final AOTSnapshotter snapshotter = AOTSnapshotter(
-      reportTimings: false,
-      fileSystem: globals.fs,
-      logger: globals.logger,
-      xcode: globals.xcode,
-      artifacts: globals.artifacts,
-      processManager: globals.processManager
-    );
-    final int result = await snapshotter.build(
+    final int result = await AOTSnapshotter(reportTimings: false).build(
       bitcode: false,
       buildMode: buildMode,
       mainPath: environment.buildDir.childFile('app.dill').path,
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index f54ec5c51e..fb5694255d 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -27,6 +27,11 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
         allowed: <String>['android-arm', 'android-arm64', 'ios', 'android-x64'],
       )
       ..addFlag('quiet', defaultsTo: false)
+      ..addFlag('report-timings',
+        negatable: false,
+        defaultsTo: false,
+        help: 'Report timing information about build steps in machine readable form,',
+      )
       ..addMultiOption('ios-arch',
         splitCommas: true,
         defaultsTo: defaultIOSArchs.map<String>(getNameForDarwinArch),
@@ -75,6 +80,7 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
       mainDartFile: findMainDartFile(targetFile),
       bitcode: boolArg('bitcode'),
       quiet: boolArg('quiet'),
+      reportTimings: boolArg('report-timings'),
       iosBuildArchs: stringsArg('ios-arch').map<DarwinArch>(getIOSArchForName),
     );
     return FlutterCommandResult.success();
diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
index 1784a906d3..dd0943776d 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
@@ -436,6 +436,7 @@ end
         mainDartFile: globals.fs.path.absolute(targetFile),
         quiet: true,
         bitcode: true,
+        reportTimings: false,
         iosBuildArchs: <DarwinArch>[DarwinArch.armv7, DarwinArch.arm64],
       );
     } finally {
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index beb2100248..07ad8c8a03 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -11,6 +11,7 @@ import 'android/gradle_utils.dart';
 import 'application_package.dart';
 import 'artifacts.dart';
 import 'asset.dart';
+import 'base/build.dart';
 import 'base/config.dart';
 import 'base/context.dart';
 import 'base/io.dart';
@@ -124,6 +125,7 @@ Future<T> runInContext<T>(
       FuchsiaDeviceTools: () => FuchsiaDeviceTools(),
       FuchsiaSdk: () => FuchsiaSdk(),
       FuchsiaWorkflow: () => FuchsiaWorkflow(),
+      GenSnapshot: () => const GenSnapshot(),
       GradleUtils: () => GradleUtils(),
       HotRunnerConfig: () => HotRunnerConfig(),
       IMobileDevice: () => IMobileDevice(),
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 09081ecf28..3bd514163a 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -249,7 +249,7 @@ class XcodeProjectInterpreter {
     @required ProcessManager processManager,
     @required Logger logger,
     @required FileSystem fileSystem,
-    @required Terminal terminal,
+    @required AnsiTerminal terminal,
   }) : _platform = platform,
        _fileSystem = fileSystem,
        _terminal = terminal,
@@ -259,7 +259,7 @@ class XcodeProjectInterpreter {
   final Platform _platform;
   final FileSystem _fileSystem;
   final ProcessUtils _processUtils;
-  final Terminal _terminal;
+  final AnsiTerminal _terminal;
   final Logger _logger;
 
   static const String _executable = '/usr/bin/xcodebuild';
diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart
index 00a4625a0a..6ea4d69114 100644
--- a/packages/flutter_tools/test/general.shard/base/build_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/build_test.dart
@@ -2,70 +2,75 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:file/memory.dart';
-import 'package:mockito/mockito.dart';
-import 'package:platform/platform.dart';
+import 'dart:async';
+import 'dart:convert';
 
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/android/android_sdk.dart';
 import 'package:flutter_tools/src/artifacts.dart';
-import 'package:flutter_tools/src/base/logger.dart';
-import 'package:flutter_tools/src/base/terminal.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/base/build.dart';
-import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/process.dart';
 import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:flutter_tools/src/globals.dart' as globals;
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
 
 import '../../src/common.dart';
 import '../../src/context.dart';
 
-const FakeCommand kSdkPathCommand = FakeCommand(
-  command: <String>[
-    'xcrun',
-    '--sdk',
-    'iphoneos',
-    '--show-sdk-path'
-  ]
-);
-
-const List<String> kDefaultClang = <String>[
-  '-miphoneos-version-min=8.0',
-  '-dynamiclib',
-  '-Xlinker',
-  '-rpath',
-  '-Xlinker',
-  '@executable_path/Frameworks',
-  '-Xlinker',
-  '-rpath',
-  '-Xlinker',
-  '@loader_path/Frameworks',
-  '-install_name',
-  '@rpath/App.framework/App',
-  '-isysroot',
-  '',
-  '-o',
-  'build/foo/App.framework/App',
-  'build/foo/snapshot_assembly.o',
-];
-
-const List<String> kBitcodeClang = <String>[
-  '-miphoneos-version-min=8.0',
-  '-dynamiclib',
-  '-Xlinker',
-  '-rpath',
-  '-Xlinker',
-  '@executable_path/Frameworks',
-  '-Xlinker',
-  '-rpath',
-  '-Xlinker',
-  '@loader_path/Frameworks',
-  '-install_name',
-  '@rpath/App.framework/App',
-  '-fembed-bitcode',
-  '-isysroot',
-  '',
-  '-o',
-  'build/foo/App.framework/App',
-  'build/foo/snapshot_assembly.o',
-];
+class MockFlutterVersion extends Mock implements FlutterVersion {}
+class MockAndroidSdk extends Mock implements AndroidSdk {}
+class MockArtifacts extends Mock implements Artifacts {}
+class MockXcode extends Mock implements Xcode {}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
+
+class _FakeGenSnapshot implements GenSnapshot {
+  _FakeGenSnapshot({
+    this.succeed = true,
+  });
+
+  final bool succeed;
+  Map<String, String> outputs = <String, String>{};
+  int _callCount = 0;
+  SnapshotType _snapshotType;
+  String _depfilePath;
+  List<String> _additionalArgs;
+
+  int get callCount => _callCount;
+
+  SnapshotType get snapshotType => _snapshotType;
+
+  String get depfilePath => _depfilePath;
+
+  List<String> get additionalArgs => _additionalArgs;
+
+  @override
+  Future<int> run({
+    SnapshotType snapshotType,
+    String depfilePath,
+    DarwinArch darwinArch,
+    Iterable<String> additionalArgs = const <String>[],
+  }) async {
+    _callCount += 1;
+    _snapshotType = snapshotType;
+    _depfilePath = depfilePath;
+    _additionalArgs = additionalArgs.toList();
+
+    if (!succeed) {
+      return 1;
+    }
+    outputs.forEach((String filePath, String fileContent) {
+      globals.fs.file(filePath).writeAsString(fileContent);
+    });
+    return 0;
+  }
+}
 
 void main() {
   group('SnapshotType', () {
@@ -76,137 +81,181 @@ void main() {
       );
     });
     test('does not throw, if target platform is null', () {
-      expect(() => SnapshotType(null, BuildMode.release), returnsNormally);
+      expect(SnapshotType(null, BuildMode.release), isNotNull);
     });
   });
 
   group('GenSnapshot', () {
     GenSnapshot genSnapshot;
     MockArtifacts mockArtifacts;
-    FakeProcessManager processManager;
-    BufferLogger logger;
+    MockProcessManager mockProcessManager;
+    MockProcess mockProc;
 
     setUp(() async {
+      genSnapshot = const GenSnapshot();
       mockArtifacts = MockArtifacts();
-      logger = BufferLogger.test();
-      processManager = FakeProcessManager.list(<  FakeCommand>[]);
-      genSnapshot = GenSnapshot(
-        artifacts: mockArtifacts,
-        logger: logger,
-        processManager: processManager,
-      );
-      when(mockArtifacts.getArtifactPath(
-        any,
-        platform: anyNamed('platform'),
-        mode: anyNamed('mode'),
-      )).thenReturn('gen_snapshot');
-    });
-
-    testWithoutContext('android_x64', () async {
-      processManager.addCommand(const FakeCommand(
-        command: <String>['gen_snapshot', '--additional_arg']
-      ));
-
-      final int result = await genSnapshot.run(
-        snapshotType: SnapshotType(TargetPlatform.android_x64, BuildMode.release),
-        darwinArch: null,
-        additionalArgs: <String>['--additional_arg'],
-      );
-      expect(result, 0);
+      mockProcessManager = MockProcessManager();
+      mockProc = MockProcess();
     });
 
-    testWithoutContext('iOS armv7', () async {
-      processManager.addCommand(const FakeCommand(
-        command: <String>['gen_snapshot_armv7', '--additional_arg']
-      ));
-
-      final int result = await genSnapshot.run(
-        snapshotType: SnapshotType(TargetPlatform.ios, BuildMode.release),
-        darwinArch: DarwinArch.armv7,
-        additionalArgs: <String>['--additional_arg'],
-      );
-      expect(result, 0);
-    });
-
-    testWithoutContext('iOS arm64', () async {
-      processManager.addCommand(const FakeCommand(
-        command: <String>['gen_snapshot_arm64', '--additional_arg']
-      ));
-
-      final int result = await genSnapshot.run(
-        snapshotType: SnapshotType(TargetPlatform.ios, BuildMode.release),
-        darwinArch: DarwinArch.arm64,
-        additionalArgs: <String>['--additional_arg'],
-      );
-      expect(result, 0);
-    });
-
-    testWithoutContext('--strip filters error output from gen_snapshot', () async {
-        processManager.addCommand(FakeCommand(
-        command: const <String>['gen_snapshot', '--strip'],
-        stderr: 'ABC\n${GenSnapshot.kIgnoredWarnings.join('\n')}\nXYZ\n'
-      ));
-
-      final int result = await genSnapshot.run(
-        snapshotType: SnapshotType(TargetPlatform.android_x64, BuildMode.release),
-        darwinArch: null,
-        additionalArgs: <String>['--strip'],
-      );
-
-      expect(result, 0);
-      expect(logger.errorText, contains('ABC'));
-      for (final String ignoredWarning in GenSnapshot.kIgnoredWarnings)  {
-        expect(logger.errorText, isNot(contains(ignoredWarning)));
-      }
-      expect(logger.errorText, contains('XYZ'));
-    });
+    final Map<Type, Generator> contextOverrides = <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      ProcessManager: () => mockProcessManager,
+    };
+
+    testUsingContext('android_x64', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.genSnapshot,
+              platform: TargetPlatform.android_x64, mode: BuildMode.release))
+          .thenReturn('gen_snapshot');
+      when(mockProcessManager.start(any,
+              workingDirectory: anyNamed('workingDirectory'),
+              environment: anyNamed('environment')))
+          .thenAnswer((_) => Future<Process>.value(mockProc));
+      when(mockProc.stdout).thenAnswer((_) => const Stream<List<int>>.empty());
+      when(mockProc.stderr).thenAnswer((_) => const Stream<List<int>>.empty());
+      await genSnapshot.run(
+          snapshotType:
+              SnapshotType(TargetPlatform.android_x64, BuildMode.release),
+          darwinArch: null,
+          additionalArgs: <String>['--additional_arg']);
+      verify(mockProcessManager.start(
+        <String>[
+          'gen_snapshot',
+          '--additional_arg',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+    }, overrides: contextOverrides);
+
+    testUsingContext('iOS armv7', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.genSnapshot,
+              platform: TargetPlatform.ios, mode: BuildMode.release))
+          .thenReturn('gen_snapshot');
+      when(mockProcessManager.start(any,
+              workingDirectory: anyNamed('workingDirectory'),
+              environment: anyNamed('environment')))
+          .thenAnswer((_) => Future<Process>.value(mockProc));
+      when(mockProc.stdout).thenAnswer((_) => const Stream<List<int>>.empty());
+      when(mockProc.stderr).thenAnswer((_) => const Stream<List<int>>.empty());
+      await genSnapshot.run(
+          snapshotType: SnapshotType(TargetPlatform.ios, BuildMode.release),
+          darwinArch: DarwinArch.armv7,
+          additionalArgs: <String>['--additional_arg']);
+      verify(mockProcessManager.start(
+        <String>[
+          'gen_snapshot_armv7',
+          '--additional_arg',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')),
+      ).called(1);
+    }, overrides: contextOverrides);
+
+    testUsingContext('iOS arm64', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.genSnapshot,
+              platform: TargetPlatform.ios, mode: BuildMode.release))
+          .thenReturn('gen_snapshot');
+      when(mockProcessManager.start(any,
+              workingDirectory: anyNamed('workingDirectory'),
+              environment: anyNamed('environment')))
+          .thenAnswer((_) => Future<Process>.value(mockProc));
+      when(mockProc.stdout).thenAnswer((_) => const Stream<List<int>>.empty());
+      when(mockProc.stderr).thenAnswer((_) => const Stream<List<int>>.empty());
+      await genSnapshot.run(
+          snapshotType: SnapshotType(TargetPlatform.ios, BuildMode.release),
+          darwinArch: DarwinArch.arm64,
+          additionalArgs: <String>['--additional_arg']);
+      verify(mockProcessManager.start(
+        <String>[
+          'gen_snapshot_arm64',
+          '--additional_arg',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+    }, overrides: contextOverrides);
+
+    testUsingContext('--strip filters outputs', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.genSnapshot,
+              platform: TargetPlatform.android_x64, mode: BuildMode.release))
+          .thenReturn('gen_snapshot');
+      when(mockProcessManager.start(
+              <String>['gen_snapshot', '--strip'],
+              workingDirectory: anyNamed('workingDirectory'),
+              environment: anyNamed('environment')))
+          .thenAnswer((_) => Future<Process>.value(mockProc));
+      when(mockProc.stdout).thenAnswer((_) => const Stream<List<int>>.empty());
+      when(mockProc.stderr)
+        .thenAnswer((_) => Stream<String>.fromIterable(<String>[
+          '--ABC\n',
+          'Warning: Generating ELF library without DWARF debugging information.\n',
+          '--XYZ\n',
+        ])
+        .transform<List<int>>(utf8.encoder));
+      await genSnapshot.run(
+          snapshotType:
+              SnapshotType(TargetPlatform.android_x64, BuildMode.release),
+          darwinArch: null,
+          additionalArgs: <String>['--strip']);
+      verify(mockProcessManager.start(
+              <String>['gen_snapshot', '--strip'],
+              workingDirectory: anyNamed('workingDirectory'),
+              environment: anyNamed('environment')))
+          .called(1);
+      expect(testLogger.errorText, contains('ABC'));
+      expect(testLogger.errorText, isNot(contains('ELF library')));
+      expect(testLogger.errorText, contains('XYZ'));
+    }, overrides: contextOverrides);
   });
 
-  group('AOTSnapshotter', () {
-    MemoryFileSystem fileSystem;
+  group('Snapshotter - AOT', () {
+    const String kSnapshotDart = 'snapshot.dart';
+    const String kSDKPath = '/path/to/sdk';
+    String skyEnginePath;
+
+    _FakeGenSnapshot genSnapshot;
+    MemoryFileSystem fs;
     AOTSnapshotter snapshotter;
+    AOTSnapshotter snapshotterWithTimings;
+    MockAndroidSdk mockAndroidSdk;
     MockArtifacts mockArtifacts;
-    FakeProcessManager processManager;
-    Logger logger;
+    MockXcode mockXcode;
 
     setUp(() async {
-      final Platform platform = FakePlatform(operatingSystem: 'macos');
-      logger = BufferLogger.test();
-      fileSystem = MemoryFileSystem.test();
+      fs = MemoryFileSystem();
+      fs.file(kSnapshotDart).createSync();
+      fs.file('.packages').writeAsStringSync('sky_engine:file:///flutter/bin/cache/pkg/sky_engine/lib/');
+
+      skyEnginePath = fs.path.fromUri(Uri.file('/flutter/bin/cache/pkg/sky_engine'));
+      fs.directory(fs.path.join(skyEnginePath, 'lib', 'ui')).createSync(recursive: true);
+      fs.directory(fs.path.join(skyEnginePath, 'sdk_ext')).createSync(recursive: true);
+      fs.file(fs.path.join(skyEnginePath, '.packages')).createSync();
+      fs.file(fs.path.join(skyEnginePath, 'lib', 'ui', 'ui.dart')).createSync();
+      fs.file(fs.path.join(skyEnginePath, 'sdk_ext', 'vmservice_io.dart')).createSync();
+
+      genSnapshot = _FakeGenSnapshot();
+      snapshotter = AOTSnapshotter();
+      snapshotterWithTimings = AOTSnapshotter(reportTimings: true);
+      mockAndroidSdk = MockAndroidSdk();
       mockArtifacts = MockArtifacts();
-      processManager = FakeProcessManager.list(<FakeCommand>[]);
-      snapshotter = AOTSnapshotter(
-        fileSystem: fileSystem,
-        logger: logger,
-        xcode: Xcode(
-          fileSystem: fileSystem,
-          logger: logger,
-          platform: FakePlatform(operatingSystem: 'macos'),
-          processManager: processManager,
-          xcodeProjectInterpreter: XcodeProjectInterpreter(
-            platform: platform,
-            processManager: processManager,
-            logger: logger,
-            fileSystem: fileSystem,
-            terminal: Terminal.test(),
-          ),
-        ),
-        artifacts: mockArtifacts,
-        processManager: processManager,
-      );
-      when(mockArtifacts.getArtifactPath(
-        Artifact.genSnapshot,
-        platform: anyNamed('platform'),
-        mode: anyNamed('mode')),
-      ).thenReturn('gen_snapshot');
+      mockXcode = MockXcode();
+      when(mockXcode.sdkLocation(any)).thenAnswer((_) => Future<String>.value(kSDKPath));
     });
 
-    testWithoutContext('does not build iOS with debug build mode', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-
+    final Map<Type, Generator> contextOverrides = <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Artifacts: () => mockArtifacts,
+      FileSystem: () => fs,
+      ProcessManager: () => FakeProcessManager.any(),
+      GenSnapshot: () => genSnapshot,
+      Xcode: () => mockXcode,
+    };
+
+    testUsingContext('iOS debug AOT snapshot is invalid', () async {
+      final String outputPath = globals.fs.path.join('build', 'foo');
       expect(await snapshotter.build(
         platform: TargetPlatform.ios,
-        darwinArch: DarwinArch.arm64,
         buildMode: BuildMode.debug,
         mainPath: 'main.dill',
         packagesPath: '.packages',
@@ -215,11 +264,10 @@ void main() {
         splitDebugInfo: null,
         dartObfuscation: false,
       ), isNot(equals(0)));
-    });
-
-    testWithoutContext('does not build android-arm with debug build mode', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
+    }, overrides: contextOverrides);
 
+    testUsingContext('Android arm debug AOT snapshot is invalid', () async {
+      final String outputPath = globals.fs.path.join('build', 'foo');
       expect(await snapshotter.build(
         platform: TargetPlatform.android_arm,
         buildMode: BuildMode.debug,
@@ -230,11 +278,10 @@ void main() {
         splitDebugInfo: null,
         dartObfuscation: false,
       ), isNot(0));
-    });
-
-    testWithoutContext('does not build android-arm64 with debug build mode', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
+    }, overrides: contextOverrides);
 
+    testUsingContext('Android arm64 debug AOT snapshot is invalid', () async {
+      final String outputPath = globals.fs.path.join('build', 'foo');
       expect(await snapshotter.build(
         platform: TargetPlatform.android_arm64,
         buildMode: BuildMode.debug,
@@ -245,50 +292,22 @@ void main() {
         splitDebugInfo: null,
         dartObfuscation: false,
       ), isNot(0));
-    });
+    }, overrides: contextOverrides);
 
-    testWithoutContext('builds iOS with bitcode', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      final String assembly = fileSystem.path.join(outputPath, 'snapshot_assembly.S');
-      processManager.addCommand(FakeCommand(
-        command: <String>[
-          'gen_snapshot_armv7',
-          '--deterministic',
-          '--snapshot_kind=app-aot-assembly',
-          '--assembly=$assembly',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          'main.dill',
-        ]
-      ));
-      processManager.addCommand(kSdkPathCommand);
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'cc',
-          '-arch',
-          'armv7',
-          '-isysroot',
-          '',
-          '-fembed-bitcode',
-          '-c',
-          'build/foo/snapshot_assembly.S',
-          '-o',
-          'build/foo/snapshot_assembly.o',
-        ]
-      ));
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'clang',
-          '-arch',
-          'armv7',
-          ...kBitcodeClang,
-        ]
-      ));
+    testUsingContext('iOS profile AOT with bitcode uses right flags', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      final String assembly = globals.fs.path.join(outputPath, 'snapshot_assembly.S');
+      genSnapshot.outputs = <String, String>{
+        assembly: 'blah blah\n.section __DWARF\nblah blah\n',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.ios,
@@ -303,53 +322,115 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=$assembly',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+
+      final VerificationResult toVerifyCC = verify(mockXcode.cc(captureAny));
+      expect(toVerifyCC.callCount, 1);
+      final dynamic ccArgs = toVerifyCC.captured.first;
+      expect(ccArgs, contains('-fembed-bitcode'));
+      expect(ccArgs, contains('-isysroot'));
+      expect(ccArgs, contains(kSDKPath));
+
+      final VerificationResult toVerifyClang = verify(mockXcode.clang(captureAny));
+      expect(toVerifyClang.callCount, 1);
+      final dynamic clangArgs = toVerifyClang.captured.first;
+      expect(clangArgs, contains('-fembed-bitcode'));
+      expect(clangArgs, contains('-isysroot'));
+      expect(clangArgs, contains(kSDKPath));
+
+      final File assemblyFile = globals.fs.file(assembly);
+      expect(assemblyFile.existsSync(), true);
+      expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
+    }, overrides: contextOverrides);
+
+    testUsingContext('iOS release AOT with bitcode uses right flags', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      final String assembly = globals.fs.path.join(outputPath, 'snapshot_assembly.S');
+      genSnapshot.outputs = <String, String>{
+        assembly: 'blah blah\n',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
-    testWithoutContext('builds iOS armv7 snapshot with dwarStackTraces', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      final String assembly = fileSystem.path.join(outputPath, 'snapshot_assembly.S');
-      final String debugPath = fileSystem.path.join('foo', 'app.ios-armv7.symbols');
-        processManager.addCommand(FakeCommand(
-        command: <String>[
-          'gen_snapshot_armv7',
-          '--deterministic',
-          '--snapshot_kind=app-aot-assembly',
-          '--assembly=$assembly',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          '--dwarf-stack-traces',
-          '--save-debugging-info=$debugPath',
-          'main.dill',
-        ]
-      ));
-      processManager.addCommand(kSdkPathCommand);
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'cc',
-          '-arch',
-          'armv7',
-          '-isysroot',
-          '',
-          '-c',
-          'build/foo/snapshot_assembly.S',
-          '-o',
-          'build/foo/snapshot_assembly.o',
-        ]
-      ));
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'clang',
-          '-arch',
-          'armv7',
-          ...kDefaultClang,
-        ]
-      ));
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        darwinArch: DarwinArch.armv7,
+        bitcode: true,
+        splitDebugInfo: null,
+        dartObfuscation: false,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=$assembly',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+
+      final VerificationResult toVerifyCC = verify(mockXcode.cc(captureAny));
+      expect(toVerifyCC.callCount, 1);
+      final dynamic ccArgs = toVerifyCC.captured.first;
+      expect(ccArgs, contains('-fembed-bitcode'));
+      expect(ccArgs, contains('-isysroot'));
+      expect(ccArgs, contains(kSDKPath));
+
+      final VerificationResult toVerifyClang = verify(mockXcode.clang(captureAny));
+      expect(toVerifyClang.callCount, 1);
+      final dynamic clangArgs = toVerifyClang.captured.first;
+      expect(clangArgs, contains('-fembed-bitcode'));
+      expect(clangArgs, contains('-isysroot'));
+      expect(clangArgs, contains(kSDKPath));
+
+      final File assemblyFile = globals.fs.file(assembly);
+      expect(assemblyFile.existsSync(), true);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS armv7 profile AOT snapshot', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      final String assembly = globals.fs.path.join(outputPath, 'snapshot_assembly.S');
+      genSnapshot.outputs = <String, String>{
+        assembly: 'blah blah\n.section __DWARF\nblah blah\n',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.ios,
@@ -359,56 +440,106 @@ void main() {
         outputPath: outputPath,
         darwinArch: DarwinArch.armv7,
         bitcode: false,
-        splitDebugInfo: 'foo',
+        splitDebugInfo: null,
         dartObfuscation: false,
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=$assembly',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+      verifyNever(mockXcode.cc(argThat(contains('-fembed-bitcode'))));
+      verifyNever(mockXcode.clang(argThat(contains('-fembed-bitcode'))));
+
+      verify(mockXcode.cc(argThat(contains('-isysroot')))).called(1);
+      verify(mockXcode.clang(argThat(contains('-isysroot')))).called(1);
+
+      final File assemblyFile = globals.fs.file(assembly);
+      expect(assemblyFile.existsSync(), true);
+      expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS armv7 profile AOT snapshot with dwarStackTraces', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      final String assembly = globals.fs.path.join(outputPath, 'snapshot_assembly.S');
+      genSnapshot.outputs = <String, String>{
+        assembly: 'blah blah\n.section __DWARF\nblah blah\n',
+      };
+      final String debugPath = globals.fs.path.join('foo', 'app.ios-armv7.symbols');
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
-    testWithoutContext('builds iOS armv7 snapshot with obfuscate', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      final String assembly = fileSystem.path.join(outputPath, 'snapshot_assembly.S');
-      processManager.addCommand(FakeCommand(
-        command: <String>[
-          'gen_snapshot_armv7',
-          '--deterministic',
-          '--snapshot_kind=app-aot-assembly',
-          '--assembly=$assembly',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          '--obfuscate',
-          'main.dill',
-        ]
-      ));
-      processManager.addCommand(kSdkPathCommand);
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'cc',
-          '-arch',
-          'armv7',
-          '-isysroot',
-          '',
-          '-c',
-          'build/foo/snapshot_assembly.S',
-          '-o',
-          'build/foo/snapshot_assembly.o',
-        ]
-      ));
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'clang',
-          '-arch',
-          'armv7',
-          ...kDefaultClang,
-        ]
-      ));
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.profile,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        darwinArch: DarwinArch.armv7,
+        bitcode: false,
+        splitDebugInfo: 'foo',
+        dartObfuscation: false,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=$assembly',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        '--dwarf-stack-traces',
+        '--save-debugging-info=$debugPath',
+        'main.dill',
+      ]);
+      verifyNever(mockXcode.cc(argThat(contains('-fembed-bitcode'))));
+      verifyNever(mockXcode.clang(argThat(contains('-fembed-bitcode'))));
+
+      verify(mockXcode.cc(argThat(contains('-isysroot')))).called(1);
+      verify(mockXcode.clang(argThat(contains('-isysroot')))).called(1);
+
+      final File assemblyFile = globals.fs.file(assembly);
+      expect(assemblyFile.existsSync(), true);
+      expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS armv7 profile AOT snapshot with obfuscate', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      final String assembly = globals.fs.path.join(outputPath, 'snapshot_assembly.S');
+      genSnapshot.outputs = <String, String>{
+        assembly: 'blah blah\n.section __DWARF\nblah blah\n',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.ios,
@@ -423,50 +554,86 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=$assembly',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        '--obfuscate',
+        'main.dill',
+      ]);
+      verifyNever(mockXcode.cc(argThat(contains('-fembed-bitcode'))));
+      verifyNever(mockXcode.clang(argThat(contains('-fembed-bitcode'))));
+
+      verify(mockXcode.cc(argThat(contains('-isysroot')))).called(1);
+      verify(mockXcode.clang(argThat(contains('-isysroot')))).called(1);
+
+      final File assemblyFile = globals.fs.file(assembly);
+      expect(assemblyFile.existsSync(), true);
+      expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS arm64 profile AOT snapshot', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        globals.fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.profile,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        darwinArch: DarwinArch.arm64,
+        bitcode: false,
+        splitDebugInfo: null,
+        dartObfuscation: false,
+      );
 
-    testWithoutContext('builds iOS armv7 snapshot', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      processManager.addCommand(FakeCommand(
-        command: <String>[
-          'gen_snapshot_armv7',
-          '--deterministic',
-          '--snapshot_kind=app-aot-assembly',
-          '--assembly=${fileSystem.path.join(outputPath, 'snapshot_assembly.S')}',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          'main.dill',
-        ]
-      ));
-      processManager.addCommand(kSdkPathCommand);
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'cc',
-          '-arch',
-          'armv7',
-          '-isysroot',
-          '',
-          '-c',
-          'build/foo/snapshot_assembly.S',
-          '-o',
-          'build/foo/snapshot_assembly.o',
-        ]
-      ));
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'clang',
-          '-arch',
-          'armv7',
-          ...kDefaultClang,
-        ]
-      ));
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=${globals.fs.path.join(outputPath, 'snapshot_assembly.S')}',
+        '--strip',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS release armv7 AOT snapshot', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        globals.fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.ios,
@@ -481,47 +648,35 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
-
-    testWithoutContext('builds iOS arm64 snapshot', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      processManager.addCommand(FakeCommand(
-        command: <String>[
-          'gen_snapshot_arm64',
-          '--deterministic',
-          '--snapshot_kind=app-aot-assembly',
-          '--assembly=${fileSystem.path.join(outputPath, 'snapshot_assembly.S')}',
-          '--strip',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          'main.dill',
-        ]
-      ));
-      processManager.addCommand(kSdkPathCommand);
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'cc',
-          '-arch',
-          'arm64',
-          '-isysroot',
-          '',
-          '-c',
-          'build/foo/snapshot_assembly.S',
-          '-o',
-          'build/foo/snapshot_assembly.o',
-        ]
-      ));
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'xcrun',
-          'clang',
-          '-arch',
-          'arm64',
-          ...kDefaultClang,
-        ]
-      ));
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=${globals.fs.path.join(outputPath, 'snapshot_assembly.S')}',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS release arm64 AOT snapshot', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        globals.fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.ios,
@@ -536,25 +691,25 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
-
-    testWithoutContext('builds shared library for android-arm (32bit)', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'gen_snapshot',
-          '--deterministic',
-          '--snapshot_kind=app-aot-elf',
-          '--elf=build/foo/app.so',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          'main.dill',
-        ]
-      ));
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=${globals.fs.path.join(outputPath, 'snapshot_assembly.S')}',
+        '--strip',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds shared library for android-arm', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.android_arm,
@@ -568,28 +723,28 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
-
-    testWithoutContext('builds shared library for android-arm with dwarf stack traces', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      final String debugPath = fileSystem.path.join('foo', 'app.android-arm.symbols');
-      processManager.addCommand(FakeCommand(
-        command: <String>[
-          'gen_snapshot',
-          '--deterministic',
-          '--snapshot_kind=app-aot-elf',
-          '--elf=build/foo/app.so',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          '--dwarf-stack-traces',
-          '--save-debugging-info=$debugPath',
-          'main.dill',
-        ]
-      ));
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-elf',
+        '--elf=build/foo/app.so',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds shared library for android-arm with dwarf stack traces', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      final String debugPath = globals.fs.path.join('foo', 'app.android-arm.symbols');
+      globals.fs.directory(outputPath).createSync(recursive: true);
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.android_arm,
@@ -603,26 +758,29 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
-
-    testWithoutContext('builds shared library for android-arm with obfuscate', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'gen_snapshot',
-          '--deterministic',
-          '--snapshot_kind=app-aot-elf',
-          '--elf=build/foo/app.so',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          '--obfuscate',
-          'main.dill',
-        ]
-      ));
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-elf',
+        '--elf=build/foo/app.so',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        '--dwarf-stack-traces',
+        '--save-debugging-info=$debugPath',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds shared library for android-arm with obfuscate', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.android_arm,
@@ -636,25 +794,28 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
-
-    testWithoutContext('builds shared library for android-arm without dwarf stack traces due to empty string', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'gen_snapshot',
-          '--deterministic',
-          '--snapshot_kind=app-aot-elf',
-          '--elf=build/foo/app.so',
-          '--strip',
-          '--no-sim-use-hardfp',
-          '--no-use-integer-division',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          'main.dill',
-        ]
-      ));
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-elf',
+        '--elf=build/foo/app.so',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        '--obfuscate',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds shared library for android-arm without dwarf stack traces due to empty string', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.android_arm,
@@ -668,23 +829,27 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-       expect(processManager.hasRemainingExpectations, false);
-    });
-
-    testWithoutContext('builds shared library for android-arm64', () async {
-      final String outputPath = fileSystem.path.join('build', 'foo');
-      processManager.addCommand(const FakeCommand(
-        command: <String>[
-          'gen_snapshot',
-          '--deterministic',
-          '--snapshot_kind=app-aot-elf',
-          '--elf=build/foo/app.so',
-          '--strip',
-          '--no-causal-async-stacks',
-          '--lazy-async-stacks',
-          'main.dill',
-        ]
-      ));
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-elf',
+        '--elf=build/foo/app.so',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds shared library for android-arm64', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
 
       final int genSnapshotExitCode = await snapshotter.build(
         platform: TargetPlatform.android_arm64,
@@ -698,9 +863,48 @@ void main() {
       );
 
       expect(genSnapshotExitCode, 0);
-      expect(processManager.hasRemainingExpectations, false);
-    });
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm64);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-elf',
+        '--elf=build/foo/app.so',
+        '--strip',
+        '--no-causal-async-stacks',
+        '--lazy-async-stacks',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('reports timing', () async {
+      globals.fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = globals.fs.path.join('build', 'foo');
+      globals.fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        globals.fs.path.join(outputPath, 'app.so'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+
+      final int genSnapshotExitCode = await snapshotterWithTimings.build(
+        platform: TargetPlatform.android_arm,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        bitcode: false,
+        splitDebugInfo: null,
+        dartObfuscation: false,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(testLogger.statusText, matches(RegExp(r'snapshot\(CompileTime\): \d+ ms.')));
+    }, overrides: contextOverrides);
   });
 }
-
-class MockArtifacts extends Mock implements Artifacts {}
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
index e0ebb090fa..0cee898d37 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
@@ -5,6 +5,7 @@
 import 'package:flutter_tools/src/base/build.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/process.dart';
 import 'package:flutter_tools/src/build_system/build_system.dart';
 import 'package:flutter_tools/src/build_system/targets/dart.dart';
 import 'package:flutter_tools/src/build_system/targets/macos.dart';
@@ -48,6 +49,7 @@ void main() {
   Testbed testbed;
   Environment environment;
   MockPlatform mockPlatform;
+  MockXcode mockXcode;
 
   setUpAll(() {
     Cache.disableLocking();
@@ -55,6 +57,7 @@ void main() {
   });
 
   setUp(() {
+    mockXcode = MockXcode();
     mockPlatform = MockPlatform();
     when(mockPlatform.isWindows).thenReturn(false);
     when(mockPlatform.isMacOS).thenReturn(true);
@@ -190,6 +193,35 @@ void main() {
 
     expect(outputFramework.readAsStringSync(), 'DEF');
   }));
+
+  test('release/profile macOS compilation uses correct gen_snapshot', () => testbed.run(() async {
+    when(genSnapshot.run(
+      snapshotType: anyNamed('snapshotType'),
+      additionalArgs: anyNamed('additionalArgs'),
+      darwinArch: anyNamed('darwinArch'),
+    )).thenAnswer((Invocation invocation) {
+      environment.buildDir.childFile('snapshot_assembly.o').createSync();
+      environment.buildDir.childFile('snapshot_assembly.S').createSync();
+      return Future<int>.value(0);
+    });
+    when(mockXcode.cc(any)).thenAnswer((Invocation invocation) {
+      return Future<RunResult>.value(RunResult(FakeProcessResult()..exitCode = 0, <String>['test']));
+    });
+    when(mockXcode.clang(any)).thenAnswer((Invocation invocation) {
+      return Future<RunResult>.value(RunResult(FakeProcessResult()..exitCode = 0, <String>['test']));
+    });
+    environment.buildDir.childFile('app.dill').createSync(recursive: true);
+    globals.fs.file('.packages')
+      ..createSync()
+      ..writeAsStringSync('''
+# Generated
+sky_engine:file:///bin/cache/pkg/sky_engine/lib/
+flutter_tools:lib/''');
+    await const CompileMacOSFramework().build(environment..defines[kBuildMode] = 'release');
+  }, overrides: <Type, Generator>{
+    GenSnapshot: () => MockGenSnapshot(),
+    Xcode: () => mockXcode,
+  }));
 }
 
 class MockPlatform extends Mock implements Platform {}
-- 
2.21.0