Version 2.14.0-107.0.dev

Merge commit '33aa207efd4f51bfa9bf0c355c62a8099a221cf3' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index 2f5efdc..ac72d20 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -253,6 +253,18 @@
       "languageVersion": "2.3"
     },
     {
+      "name": "devtools_server",
+      "rootUri": "../third_party/devtools/devtools_server",
+      "packageUri": "lib/",
+      "languageVersion": "2.6"
+    },
+    {
+      "name": "devtools_shared",
+      "rootUri": "../third_party/devtools/devtools_shared",
+      "packageUri": "lib/",
+      "languageVersion": "2.3"
+    },
+    {
       "name": "diagnostic",
       "rootUri": "../pkg/diagnostic",
       "packageUri": "lib/",
diff --git a/.packages b/.packages
index 7cbfb89..1631e03 100644
--- a/.packages
+++ b/.packages
@@ -21,6 +21,7 @@
 benchmark_harness:third_party/pkg/benchmark_harness/lib
 boolean_selector:third_party/pkg/boolean_selector/lib
 build_integration:pkg/build_integration/lib
+browser_launcher:third_party/pkg/browser_launcher/lib
 charcode:third_party/pkg/charcode/lib
 cli_util:third_party/pkg/cli_util/lib
 collection:third_party/pkg/collection/lib
@@ -37,6 +38,7 @@
 dartdoc:third_party/pkg/dartdoc/lib
 dds:pkg/dds/lib
 dev_compiler:pkg/dev_compiler/lib
+devtools_shared:third_party/devtools/devtools_shared/lib
 diagnostic:pkg/diagnostic/lib
 expect:pkg/expect/lib
 ffi:third_party/pkg/ffi/lib
diff --git a/DEPS b/DEPS
index d17ac02..1505b2a 100644
--- a/DEPS
+++ b/DEPS
@@ -80,6 +80,7 @@
   "boringssl_gen_rev": "7322fc15cc065d8d2957fccce6b62a509dc4d641",
   "boringssl_rev" : "1607f54fed72c6589d560254626909a64124f091",
   "browser-compat-data_tag": "v1.0.22",
+  "browser_launcher_rev": "12ab9f351a44ac803de9bc17bb2180bb312a9dd7",
   "charcode_rev": "bcd8a12c315b7a83390e4865ad847ecd9344cba2",
   "chrome_rev" : "19997",
   "cli_util_rev" : "8c504de5deb08fe32ecf51f9662bb37d8c708e57",
@@ -105,7 +106,6 @@
   "dart_style_rev": "f17c23e0eea9a870601c19d904e2a9c1a7c81470",
 
   "chromedriver_tag": "83.0.4103.39",
-  "browser_launcher_rev": "12ab9f351a44ac803de9bc17bb2180bb312a9dd7",
   "dartdoc_rev" : "505f163f7cb48e917503e4a23fbff1227e08b263",
   "devtools_rev" : "12ad5341ae0a275042c84a4e7be9a6c98db65612",
   "jsshell_tag": "version:88.0",
@@ -320,6 +320,9 @@
       Var('chromium_git') + '/external/github.com/mdn/browser-compat-data' +
       "@" + Var("browser-compat-data_tag"),
 
+  Var("dart_root") + "/third_party/pkg/browser_launcher":
+      Var("dart_git") + "browser_launcher.git" + "@" + Var("browser_launcher_rev"),
+
   Var("dart_root") + "/third_party/tcmalloc/gperftools":
       Var('chromium_git') + '/external/github.com/gperftools/gperftools.git' +
       "@" + Var("gperftools_revision"),
@@ -336,9 +339,6 @@
   Var("dart_root") + "/third_party/pkg/boolean_selector":
       Var("dart_git") + "boolean_selector.git" +
       "@" + Var("boolean_selector_rev"),
-  Var("dart_root") + "/third_party/pkg/browser_launcher":
-      Var("dart_git") + "browser_launcher.git" +
-      "@" + Var("browser_launcher_rev"),
   Var("dart_root") + "/third_party/pkg/charcode":
       Var("dart_git") + "charcode.git" + "@" + Var("charcode_rev"),
   Var("dart_root") + "/third_party/pkg/cli_util":
diff --git a/pkg/analyzer/tool/diagnostics/diagnostics.md b/pkg/analyzer/tool/diagnostics/diagnostics.md
index 1722c990..6b5ed50 100644
--- a/pkg/analyzer/tool/diagnostics/diagnostics.md
+++ b/pkg/analyzer/tool/diagnostics/diagnostics.md
@@ -398,8 +398,7 @@
 
 ### ambiguous_extension_member_access
 
-_A member named '{0}' is defined in extensions {1}, and neither is more
-specific._
+_A member named '{0}' is defined in extensions {1}, and none are more specific._
 
 #### Description
 
diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart
index b72fbba..5e581c5 100644
--- a/pkg/dartdev/lib/dartdev.dart
+++ b/pkg/dartdev/lib/dartdev.dart
@@ -40,7 +40,8 @@
     args = args
         .where(
           (element) => !(element.contains('--observe') ||
-              element.contains('--enable-vm-service')),
+              element.contains('--enable-vm-service') ||
+              element.contains('--devtools')),
         )
         .toList();
   }
diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart
index db32bfe..d996c3c 100644
--- a/pkg/dartdev/lib/src/commands/run.dart
+++ b/pkg/dartdev/lib/src/commands/run.dart
@@ -158,6 +158,10 @@
         hide: !verbose,
         negatable: false,
         help: 'Enables tracing of library and script loading.',
+      )
+      ..addFlag(
+        'debug-dds',
+        hide: true,
       );
     addExperimentalFlags(argParser, verbose);
   }
@@ -179,13 +183,18 @@
     String launchDdsArg = argResults['launch-dds'];
     String ddsHost = '';
     String ddsPort = '';
+
+    // TODO(bkonyi): allow for users to choose not to launch DevTools
+    // See https://github.com/dart-lang/sdk/issues/45867.
+    const bool launchDevTools = true;
     bool launchDds = false;
     if (launchDdsArg != null) {
       launchDds = true;
-      final ddsUrl = launchDdsArg.split(':');
+      final ddsUrl = launchDdsArg.split('\\:');
       ddsHost = ddsUrl[0];
       ddsPort = ddsUrl[1];
     }
+    final bool debugDds = argResults['debug-dds'];
 
     bool disableServiceAuthCodes = argResults['disable-service-auth-codes'];
 
@@ -198,7 +207,12 @@
     if (launchDds) {
       debugSession = _DebuggingSession();
       if (!await debugSession.start(
-          ddsHost, ddsPort, disableServiceAuthCodes)) {
+        ddsHost,
+        ddsPort,
+        disableServiceAuthCodes,
+        launchDevTools,
+        debugDds,
+      )) {
         return errorExitCode;
       }
     }
@@ -242,10 +256,19 @@
 
 class _DebuggingSession {
   Future<bool> start(
-      String host, String port, bool disableServiceAuthCodes) async {
-    final ddsSnapshot = (dirname(sdk.dart).endsWith('bin'))
+    String host,
+    String port,
+    bool disableServiceAuthCodes,
+    bool enableDevTools,
+    bool debugDds,
+  ) async {
+    final sdkDir = dirname(sdk.dart);
+    final fullSdk = sdkDir.endsWith('bin');
+    final ddsSnapshot = fullSdk
         ? sdk.ddsSnapshot
-        : absolute(dirname(sdk.dart), 'gen', 'dds.dart.snapshot');
+        : absolute(sdkDir, 'gen', 'dds.dart.snapshot');
+    final devToolsBinaries =
+        fullSdk ? sdk.devToolsBinaries : absolute(sdkDir, 'devtools');
     if (!Sdk.checkArtifactExists(ddsSnapshot)) {
       return false;
     }
@@ -256,30 +279,51 @@
       serviceInfo = await Service.getInfo();
     }
     final process = await Process.start(
-        sdk.dart,
-        [
-          if (dirname(sdk.dart).endsWith('bin'))
-            sdk.ddsSnapshot
-          else
-            absolute(dirname(sdk.dart), 'gen', 'dds.dart.snapshot'),
-          serviceInfo.serverUri.toString(),
-          host,
-          port,
-          disableServiceAuthCodes.toString(),
-        ],
-        mode: ProcessStartMode.detachedWithStdio);
+      sdk.dart,
+      [
+        if (debugDds) '--enable-vm-service=0',
+        ddsSnapshot,
+        serviceInfo.serverUri.toString(),
+        host,
+        port,
+        disableServiceAuthCodes.toString(),
+        enableDevTools.toString(),
+        devToolsBinaries,
+        debugDds.toString(),
+      ],
+      mode: ProcessStartMode.detachedWithStdio,
+    );
     final completer = Completer<void>();
-    StreamSubscription sub;
-    sub = process.stderr.transform(utf8.decoder).listen((event) {
-      if (event == 'DDS started') {
-        sub.cancel();
+    const devToolsMessagePrefix =
+        'The Dart DevTools debugger and profiler is available at:';
+    if (debugDds) {
+      StreamSubscription stdoutSub;
+      stdoutSub = process.stdout.transform(utf8.decoder).listen((event) {
+        if (event.startsWith(devToolsMessagePrefix)) {
+          final ddsDebuggingUri = event.split(' ').last;
+          print(
+            'A DevTools debugger for DDS is available at: $ddsDebuggingUri',
+          );
+          stdoutSub.cancel();
+        }
+      });
+    }
+    StreamSubscription stderrSub;
+    stderrSub = process.stderr.transform(utf8.decoder).listen((event) {
+      final result = json.decode(event) as Map<String, dynamic>;
+      final state = result['state'];
+      if (state == 'started') {
+        if (result.containsKey('devToolsUri')) {
+          final devToolsUri = result['devToolsUri'];
+          print('$devToolsMessagePrefix $devToolsUri');
+        }
+        stderrSub.cancel();
         completer.complete();
-      } else if (event.contains('Failed to start DDS')) {
-        sub.cancel();
-        completer.completeError(event.replaceAll(
-          'Failed to start DDS',
+      } else {
+        stderrSub.cancel();
+        completer.completeError(
           'Could not start Observatory HTTP server',
-        ));
+        );
       }
     });
     try {
diff --git a/pkg/dartdev/lib/src/sdk.dart b/pkg/dartdev/lib/src/sdk.dart
index 3e8a12a..426f810 100644
--- a/pkg/dartdev/lib/src/sdk.dart
+++ b/pkg/dartdev/lib/src/sdk.dart
@@ -68,6 +68,13 @@
         'dds.dart.snapshot',
       );
 
+  String get devToolsBinaries => path.absolute(
+        sdkPath,
+        'bin',
+        'resources',
+        'devtools',
+      );
+
   String get pubSnapshot => path.absolute(
         sdkPath,
         'bin',
diff --git a/pkg/dartdev/test/commands/run_test.dart b/pkg/dartdev/test/commands/run_test.dart
index b8cc5e8..58a4d56 100644
--- a/pkg/dartdev/test/commands/run_test.dart
+++ b/pkg/dartdev/test/commands/run_test.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as path;
@@ -300,4 +302,61 @@
     expect(result.stderr, isEmpty);
     expect(result.exitCode, 0);
   });
+
+  group('DevTools', () {
+    const devToolsMessagePrefix =
+        'The Dart DevTools debugger and profiler is available at: http://127.0.0.1:';
+
+    test('spawn simple', () async {
+      p = project(mainSrc: "void main() { print('Hello World'); }");
+      ProcessResult result = p.runSync([
+        'run',
+        '--enable-vm-service',
+        p.relativeFilePath,
+      ]);
+      expect(result.stdout, contains(devToolsMessagePrefix));
+    });
+
+    test('implicit spawn', () async {
+      p = project(mainSrc: "void main() { print('Hello World'); }");
+      ProcessResult result = p.runSync([
+        '--enable-vm-service',
+        p.relativeFilePath,
+      ]);
+      expect(result.stdout, contains(devToolsMessagePrefix));
+    });
+
+    test(
+      'spawn via SIGQUIT',
+      () async {
+        p = project(
+          mainSrc:
+              'void main() { print("ready"); int i = 0; while(true) { i++; } }',
+        );
+        Process process = await p.start([
+          p.relativeFilePath,
+        ]);
+
+        final readyCompleter = Completer<void>();
+        final completer = Completer<void>();
+
+        StreamSubscription sub;
+        sub = process.stdout.transform(utf8.decoder).listen((event) async {
+          if (event.contains('ready')) {
+            readyCompleter.complete();
+          } else if (event.contains(devToolsMessagePrefix)) {
+            await sub.cancel();
+            completer.complete();
+          }
+        });
+        // Wait for process to start.
+        await readyCompleter.future;
+        process.kill(ProcessSignal.sigquit);
+        await completer.future;
+        process.kill();
+      },
+      // No support for SIGQUIT on Windows.
+      skip: Platform.isWindows,
+    );
+  });
 }
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 3db7549..b39156e 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -1,4 +1,5 @@
-# 1.7.7-dev
+# 1.8.0-dev
+- Add support for launching DevTools from DDS.
 - Fixed issue where two clients subscribing to the same stream in close succession
   could result in DDS sending multiple `streamListen` requests to the VM service.
 
diff --git a/pkg/dds/bin/dds.dart b/pkg/dds/bin/dds.dart
index 9937d26..45673f5 100644
--- a/pkg/dds/bin/dds.dart
+++ b/pkg/dds/bin/dds.dart
@@ -4,6 +4,7 @@
 
 // @dart=2.10
 
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:dds/dds.dart';
@@ -16,6 +17,9 @@
 ///   - DDS bind address
 ///   - DDS port
 ///   - Disable service authentication codes
+///   - Start DevTools
+///   - DevTools build directory
+///   - Enable logging
 Future<void> main(List<String> args) async {
   if (args.isEmpty) return;
 
@@ -37,16 +41,37 @@
     port: int.parse(args[2]),
   );
   final disableServiceAuthCodes = args[3] == 'true';
+
+  final startDevTools = args[4] == 'true';
+  Uri devToolsBuildDirectory;
+  if (args[5].isNotEmpty) {
+    devToolsBuildDirectory = Uri.file(args[5]);
+  }
+  final logRequests = args[6] == 'true';
   try {
     // TODO(bkonyi): add retry logic similar to that in vmservice_server.dart
     // See https://github.com/dart-lang/sdk/issues/43192.
-    await DartDevelopmentService.startDartDevelopmentService(
+    final dds = await DartDevelopmentService.startDartDevelopmentService(
       remoteVmServiceUri,
       serviceUri: serviceUri,
       enableAuthCodes: !disableServiceAuthCodes,
+      devToolsConfiguration: startDevTools
+          ? DevToolsConfiguration(
+              enable: startDevTools,
+              customBuildDirectoryPath: devToolsBuildDirectory,
+            )
+          : null,
+      logRequests: logRequests,
     );
-    stderr.write('DDS started');
-  } catch (e) {
-    stderr.writeln('Failed to start DDS:\n$e');
+    stderr.write(json.encode({
+      'state': 'started',
+      if (dds.devToolsUri != null) 'devToolsUri': dds.devToolsUri.toString(),
+    }));
+  } catch (e, st) {
+    stderr.write(json.encode({
+      'state': 'error',
+      'error': '$e',
+      'stacktrace': '$st',
+    }));
   }
 }
diff --git a/pkg/dds/lib/dds.dart b/pkg/dds/lib/dds.dart
index f7c7a05..f0e913b 100644
--- a/pkg/dds/lib/dds.dart
+++ b/pkg/dds/lib/dds.dart
@@ -44,6 +44,8 @@
     Uri serviceUri,
     bool enableAuthCodes = true,
     bool ipv6 = false,
+    DevToolsConfiguration devToolsConfiguration = const DevToolsConfiguration(),
+    bool logRequests = false,
   }) async {
     if (remoteVmServiceUri == null) {
       throw ArgumentError.notNull('remoteVmServiceUri');
@@ -80,6 +82,8 @@
       serviceUri,
       enableAuthCodes,
       ipv6,
+      devToolsConfiguration,
+      logRequests,
     );
     await service.startService();
     return service;
@@ -125,6 +129,11 @@
   /// Returns `null` if the service is not running.
   Uri get wsUri;
 
+  /// The HTTP [Uri] of the hosted DevTools instance.
+  ///
+  /// Returns `null` if DevTools is not running.
+  Uri get devToolsUri;
+
   /// Set to `true` if this instance of [DartDevelopmentService] is accepting
   /// requests.
   bool get isRunning;
@@ -168,3 +177,13 @@
   final int errorCode;
   final String message;
 }
+
+class DevToolsConfiguration {
+  const DevToolsConfiguration({
+    this.enable = false,
+    this.customBuildDirectoryPath,
+  });
+
+  final bool enable;
+  final Uri customBuildDirectoryPath;
+}
diff --git a/pkg/dds/lib/src/client.dart b/pkg/dds/lib/src/client.dart
index f81bd25..9ceb162 100644
--- a/pkg/dds/lib/src/client.dart
+++ b/pkg/dds/lib/src/client.dart
@@ -21,27 +21,25 @@
 /// Representation of a single DDS client which manages the connection and
 /// DDS request intercepting / forwarding.
 class DartDevelopmentServiceClient {
-  factory DartDevelopmentServiceClient.fromWebSocket(
+  DartDevelopmentServiceClient.fromWebSocket(
     DartDevelopmentService dds,
     WebSocketChannel ws,
     json_rpc.Peer vmServicePeer,
-  ) =>
-      DartDevelopmentServiceClient._(
-        dds,
-        ws,
-        vmServicePeer,
-      );
+  ) : this._(
+          dds,
+          ws,
+          vmServicePeer,
+        );
 
-  factory DartDevelopmentServiceClient.fromSSEConnection(
+  DartDevelopmentServiceClient.fromSSEConnection(
     DartDevelopmentService dds,
     SseConnection sse,
     json_rpc.Peer vmServicePeer,
-  ) =>
-      DartDevelopmentServiceClient._(
-        dds,
-        sse,
-        vmServicePeer,
-      );
+  ) : this._(
+          dds,
+          sse,
+          vmServicePeer,
+        );
 
   DartDevelopmentServiceClient._(
     this.dds,
diff --git a/pkg/dds/lib/src/constants.dart b/pkg/dds/lib/src/constants.dart
index 2466390..b69ae7c 100644
--- a/pkg/dds/lib/src/constants.dart
+++ b/pkg/dds/lib/src/constants.dart
@@ -16,6 +16,10 @@
   };
 }
 
+// Give connections time to reestablish before considering them closed.
+// Required to reestablish connections killed by UberProxy.
+const sseKeepAlive = Duration(seconds: 30);
+
 abstract class PauseTypeMasks {
   static const pauseOnStartMask = 1 << 0;
   static const pauseOnReloadMask = 1 << 1;
diff --git a/pkg/dds/lib/src/dds_impl.dart b/pkg/dds/lib/src/dds_impl.dart
index db355fb..c28523d 100644
--- a/pkg/dds/lib/src/dds_impl.dart
+++ b/pkg/dds/lib/src/dds_impl.dart
@@ -24,6 +24,8 @@
 import 'binary_compatible_peer.dart';
 import 'client.dart';
 import 'client_manager.dart';
+import 'constants.dart';
+import 'devtools/devtools_handler.dart';
 import 'expression_evaluator.dart';
 import 'isolate_manager.dart';
 import 'stream_manager.dart';
@@ -51,7 +53,13 @@
 
 class DartDevelopmentServiceImpl implements DartDevelopmentService {
   DartDevelopmentServiceImpl(
-      this._remoteVmServiceUri, this._uri, this._authCodesEnabled, this._ipv6) {
+    this._remoteVmServiceUri,
+    this._uri,
+    this._authCodesEnabled,
+    this._ipv6,
+    this._devToolsConfiguration,
+    this.shouldLogRequests,
+  ) {
     _clientManager = ClientManager(this);
     _expressionEvaluator = ExpressionEvaluator(this);
     _isolateManager = IsolateManager(this);
@@ -113,20 +121,26 @@
         (_ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4)
             .host;
     final port = uri?.port ?? 0;
-
+    var pipeline = const Pipeline();
+    if (shouldLogRequests) {
+      pipeline = pipeline.addMiddleware(
+        logRequests(
+          logger: (String message, bool isError) {
+            print('Log: $message');
+          },
+        ),
+      );
+    }
+    pipeline = pipeline.addMiddleware(_authCodeMiddleware);
+    final handler = pipeline.addHandler(_handlers().handler);
     // Start the DDS server.
-    _server = await io.serve(
-        const Pipeline()
-            .addMiddleware(_authCodeMiddleware)
-            .addHandler(_handlers().handler),
-        host,
-        port);
+    _server = await io.serve(handler, host, port);
 
     final tmpUri = Uri(
       scheme: 'http',
       host: host,
       port: _server.port,
-      path: '$_authCode/',
+      path: '$authCode/',
     );
 
     // Notify the VM service that this client is DDS and that it should close
@@ -157,7 +171,7 @@
       return;
     }
     _shuttingDown = true;
-    // Don't accept anymore HTTP requests.
+    // Don't accept any more HTTP requests.
     await _server?.close();
 
     // Close connections to clients.
@@ -197,7 +211,7 @@
             return forbidden;
           }
           final authToken = pathSegments[0];
-          if (authToken != _authCode) {
+          if (authToken != authCode) {
             return forbidden;
           }
           // Creates a new request with the authentication code stripped from
@@ -233,18 +247,12 @@
       });
 
   Handler _sseHandler() {
-    // Give connections time to reestablish before considering them closed.
-    // Required to reestablish connections killed by UberProxy.
-    const keepAlive = Duration(seconds: 30);
-    final handler = authCodesEnabled
-        ? SseHandler(
-            Uri.parse('/$_authCode/$_kSseHandlerPath'),
-            keepAlive: keepAlive,
-          )
-        : SseHandler(
-            Uri.parse('/$_kSseHandlerPath'),
-            keepAlive: keepAlive,
-          );
+    final handler = SseHandler(
+      authCodesEnabled
+          ? Uri.parse('/$authCode/$_kSseHandlerPath')
+          : Uri.parse('/$_kSseHandlerPath'),
+      keepAlive: sseKeepAlive,
+    );
 
     handler.connections.rest.listen((sseConnection) {
       final client = DartDevelopmentServiceClient.fromSSEConnection(
@@ -259,10 +267,18 @@
   }
 
   Handler _httpHandler() {
-    // DDS doesn't support any HTTP requests itself, so we just forward all of
-    // them to the VM service.
-    final cascade = Cascade().add(proxyHandler(remoteVmServiceUri));
-    return cascade.handler;
+    if (_devToolsConfiguration != null && _devToolsConfiguration.enable) {
+      // Install the DevTools handlers and forward any unhandled HTTP requests to
+      // the VM service.
+      final buildDir =
+          _devToolsConfiguration.customBuildDirectoryPath?.toFilePath();
+      return devtoolsHandler(
+        dds: this,
+        buildDir: buildDir,
+        notFoundHandler: proxyHandler(remoteVmServiceUri),
+      );
+    }
+    return proxyHandler(remoteVmServiceUri);
   }
 
   List<String> _cleanupPathSegments(Uri uri) {
@@ -296,34 +312,63 @@
     return uri.replace(scheme: 'sse', pathSegments: pathSegments);
   }
 
+  Uri _toDevTools(Uri uri) {
+    // The DevTools URI is a bit strange as the query parameters appear after
+    // the fragment. There's no nice way to encode the query parameters
+    // properly, so we create another Uri just to grab the formatted query.
+    // The result will need to have '/?' prepended when being used as the
+    // fragment to get the correct format.
+    final query = Uri(
+      queryParameters: {
+        'uri': wsUri.toString(),
+      },
+    ).query;
+    return Uri(
+      scheme: 'http',
+      host: uri.host,
+      port: uri.port,
+      pathSegments: [
+        ...uri.pathSegments.where(
+          (e) => e.isNotEmpty,
+        ),
+        'devtools',
+        '',
+      ],
+      fragment: '/?$query',
+    );
+  }
+
   String getNamespace(DartDevelopmentServiceClient client) =>
       clientManager.clients.keyOf(client);
 
-  @override
   bool get authCodesEnabled => _authCodesEnabled;
   final bool _authCodesEnabled;
+  String get authCode => _authCode;
   String _authCode;
 
-  @override
+  final bool shouldLogRequests;
+
   Uri get remoteVmServiceUri => _remoteVmServiceUri;
 
-  @override
   Uri get remoteVmServiceWsUri => _toWebSocket(_remoteVmServiceUri);
   Uri _remoteVmServiceUri;
 
-  @override
   Uri get uri => _uri;
+  Uri _uri;
 
-  @override
   Uri get sseUri => _toSse(_uri);
 
   Uri get wsUri => _toWebSocket(_uri);
-  Uri _uri;
+
+  Uri get devToolsUri =>
+      _devToolsConfiguration.enable ? _toDevTools(_uri) : null;
 
   final bool _ipv6;
 
   bool get isRunning => _uri != null;
 
+  final DevToolsConfiguration _devToolsConfiguration;
+
   Future<void> get done => _done.future;
   Completer _done = Completer<void>();
   bool _shuttingDown = false;
diff --git a/pkg/dds/lib/src/devtools/devtools_client.dart b/pkg/dds/lib/src/devtools/devtools_client.dart
new file mode 100644
index 0000000..5f8670e
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/devtools_client.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart=2.9
+
+import 'dart:async';
+
+import 'package:json_rpc_2/src/server.dart' as json_rpc;
+import 'package:sse/src/server/sse_handler.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+import 'server_api.dart';
+
+class LoggingMiddlewareSink<S> implements StreamSink<S> {
+  LoggingMiddlewareSink(this.sink);
+
+  @override
+  void add(S event) {
+    print('DevTools SSE response: $event');
+    sink.add(event);
+  }
+
+  @override
+  void addError(Object error, [StackTrace stackTrace]) {
+    print('DevTools SSE error response: $error');
+    sink.addError(error);
+  }
+
+  @override
+  Future addStream(Stream<S> stream) {
+    return sink.addStream(stream);
+  }
+
+  @override
+  Future close() => sink.close();
+
+  @override
+  Future get done => sink.done;
+
+  final StreamSink sink;
+}
+
+/// Represents a DevTools client connection to the DevTools server API.
+class DevToolsClient {
+  DevToolsClient.fromSSEConnection(
+    SseConnection sse,
+    bool loggingEnabled,
+  ) {
+    Stream<String> stream = sse.stream;
+    StreamSink sink = sse.sink;
+
+    if (loggingEnabled) {
+      stream = stream.map<String>((String e) {
+        print('DevTools SSE request: $e');
+        return e;
+      });
+      sink = LoggingMiddlewareSink(sink);
+    }
+
+    _server = json_rpc.Server(
+      StreamChannel(stream, sink),
+      strictProtocolChecks: false,
+    );
+    _registerJsonRpcMethods();
+    _server.listen();
+  }
+
+  void _registerJsonRpcMethods() {
+    _server.registerMethod('connected', (parameters) {
+      // Nothing to do here.
+    });
+
+    _server.registerMethod('currentPage', (parameters) {
+      // Nothing to do here.
+    });
+
+    _server.registerMethod('disconnected', (parameters) {
+      // Nothing to do here.
+    });
+
+    _server.registerMethod('getPreferenceValue', (parameters) {
+      final key = parameters['key'].asString;
+      final value = ServerApi.devToolsPreferences.properties[key];
+      return value;
+    });
+
+    _server.registerMethod('setPreferenceValue', (parameters) {
+      final key = parameters['key'].asString;
+      final value = parameters['value'].value;
+      ServerApi.devToolsPreferences.properties[key] = value;
+    });
+  }
+
+  json_rpc.Server _server;
+}
diff --git a/pkg/dds/lib/src/devtools/devtools_handler.dart b/pkg/dds/lib/src/devtools/devtools_handler.dart
new file mode 100644
index 0000000..a0a4bf1
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/devtools_handler.dart
@@ -0,0 +1,87 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart=2.9
+
+import 'dart:async';
+
+import 'package:dds/src/constants.dart';
+import 'package:meta/meta.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf_static/shelf_static.dart';
+import 'package:sse/server/sse_handler.dart';
+
+import '../dds_impl.dart';
+import 'devtools_client.dart';
+import 'server_api.dart';
+
+/// Returns a [Handler] which handles serving DevTools and the DevTools server
+/// API under $DDS_URI/devtools/.
+///
+/// [buildDir] is the path to the pre-compiled DevTools instance to be served.
+///
+/// [notFoundHandler] is a [Handler] to which requests that could not be handled
+/// by the DevTools handler are forwarded (e.g., a proxy to the VM service).
+FutureOr<Handler> devtoolsHandler({
+  @required DartDevelopmentServiceImpl dds,
+  @required String buildDir,
+  @required Handler notFoundHandler,
+}) {
+  // Serves the web assets for DevTools.
+  final devtoolsAssetHandler = createStaticHandler(
+    buildDir,
+    defaultDocument: 'index.html',
+  );
+
+  // Support DevTools client-server interface via SSE.
+  // Note: the handler path needs to match the full *original* path, not the
+  // current request URL (we remove '/devtools' in the initial router but we
+  // need to include it here).
+  const devToolsSseHandlerPath = '/devtools/api/sse';
+  final devToolsApiHandler = SseHandler(
+    dds.authCodesEnabled
+        ? Uri.parse('/${dds.authCode}$devToolsSseHandlerPath')
+        : Uri.parse(devToolsSseHandlerPath),
+    keepAlive: sseKeepAlive,
+  );
+
+  devToolsApiHandler.connections.rest.listen(
+    (sseConnection) => DevToolsClient.fromSSEConnection(
+      sseConnection,
+      dds.shouldLogRequests,
+    ),
+  );
+
+  final devtoolsHandler = (Request request) {
+    // If the request isn't of the form api/<method> assume it's a request for
+    // DevTools assets.
+    if (request.url.pathSegments.length < 2 ||
+        request.url.pathSegments.first != 'api') {
+      return devtoolsAssetHandler(request);
+    }
+    final method = request.url.pathSegments[1];
+    if (method == 'ping') {
+      // Note: we have an 'OK' body response, otherwise the response has an
+      // incorrect status code (204 instead of 200).
+      return Response.ok('OK');
+    }
+    if (method == 'sse') {
+      return devToolsApiHandler.handler(request);
+    }
+    if (!ServerApi.canHandle(request)) {
+      return Response.notFound('$method is not a valid API');
+    }
+    return ServerApi.handle(request);
+  };
+
+  return (request) {
+    final pathSegments = request.url.pathSegments;
+    if (pathSegments.isEmpty || pathSegments.first != 'devtools') {
+      return notFoundHandler(request);
+    }
+    // Forward all requests to /devtools/* to the DevTools handler.
+    request = request.change(path: 'devtools');
+    return devtoolsHandler(request);
+  };
+}
diff --git a/pkg/dds/lib/src/devtools/file_system.dart b/pkg/dds/lib/src/devtools/file_system.dart
new file mode 100644
index 0000000..9a05de7
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/file_system.dart
@@ -0,0 +1,84 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart=2.9
+
+// TODO(bkonyi): remove once package:devtools_server_api is available
+// See https://github.com/flutter/devtools/issues/2958.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import 'usage.dart';
+
+class LocalFileSystem {
+  static String _userHomeDir() {
+    final String envKey =
+        Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
+    final String value = Platform.environment[envKey];
+    return value == null ? '.' : value;
+  }
+
+  /// Returns the path to the DevTools storage directory.
+  static String devToolsDir() {
+    return path.join(_userHomeDir(), '.flutter-devtools');
+  }
+
+  /// Moves the .devtools file to ~/.flutter-devtools/.devtools if the .devtools file
+  /// exists in the user's home directory.
+  static void maybeMoveLegacyDevToolsStore() {
+    final file = File(path.join(_userHomeDir(), DevToolsUsage.storeName));
+    if (file.existsSync()) {
+      ensureDevToolsDirectory();
+      file.copySync(path.join(devToolsDir(), DevToolsUsage.storeName));
+      file.deleteSync();
+    }
+  }
+
+  /// Creates the ~/.flutter-devtools directory if it does not already exist.
+  static void ensureDevToolsDirectory() {
+    Directory('${LocalFileSystem.devToolsDir()}').createSync();
+  }
+
+  /// Returns a DevTools file from the given path.
+  ///
+  /// Only files within ~/.flutter-devtools/ can be accessed.
+  static File devToolsFileFromPath(String pathFromDevToolsDir) {
+    if (pathFromDevToolsDir.contains('..')) {
+      // The passed in path should not be able to walk up the directory tree
+      // outside of the ~/.flutter-devtools/ directory.
+      return null;
+    }
+    ensureDevToolsDirectory();
+    final file = File(path.join(devToolsDir(), pathFromDevToolsDir));
+    if (!file.existsSync()) {
+      return null;
+    }
+    return file;
+  }
+
+  /// Returns a DevTools file from the given path as encoded json.
+  ///
+  /// Only files within ~/.flutter-devtools/ can be accessed.
+  static String devToolsFileAsJson(String pathFromDevToolsDir) {
+    final file = devToolsFileFromPath(pathFromDevToolsDir);
+    if (file == null) return null;
+
+    final fileName = path.basename(file.path);
+    if (!fileName.endsWith('.json')) return null;
+
+    final content = file.readAsStringSync();
+    final json = jsonDecode(content);
+    json['lastModifiedTime'] = file.lastModifiedSync().toString();
+    return jsonEncode(json);
+  }
+
+  /// Whether the flutter store file exists.
+  static bool flutterStoreExists() {
+    final flutterStore = File('${_userHomeDir()}/.flutter');
+    return flutterStore.existsSync();
+  }
+}
diff --git a/pkg/dds/lib/src/devtools/server_api.dart b/pkg/dds/lib/src/devtools/server_api.dart
new file mode 100644
index 0000000..b866f44
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/server_api.dart
@@ -0,0 +1,230 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart=2.9
+
+// TODO(bkonyi): remove once package:devtools_server_api is available
+// See https://github.com/flutter/devtools/issues/2958.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:devtools_shared/devtools_shared.dart';
+import 'package:shelf/shelf.dart' as shelf;
+
+import 'file_system.dart';
+import 'usage.dart';
+
+/// The DevTools server API.
+///
+/// This defines endpoints that serve all requests that come in over api/.
+class ServerApi {
+  static const errorNoActiveSurvey = 'ERROR: setActiveSurvey not called.';
+
+  /// Determines whether or not [request] is an API call.
+  static bool canHandle(shelf.Request request) {
+    return request.url.path.startsWith(apiPrefix);
+  }
+
+  /// Handles all requests.
+  ///
+  /// To override an API call, pass in a subclass of [ServerApi].
+  static FutureOr<shelf.Response> handle(
+    shelf.Request request, [
+    ServerApi api,
+  ]) {
+    api ??= ServerApi();
+    switch (request.url.path) {
+      // ----- Flutter Tool GA store. -----
+      case apiGetFlutterGAEnabled:
+        // Is Analytics collection enabled?
+        return api.getCompleted(
+          request,
+          json.encode(FlutterUsage.doesStoreExist ? _usage.enabled : null),
+        );
+      case apiGetFlutterGAClientId:
+        // Flutter Tool GA clientId - ONLY get Flutter's clientId if enabled is
+        // true.
+        return (FlutterUsage.doesStoreExist)
+            ? api.getCompleted(
+                request,
+                json.encode(_usage.enabled ? _usage.clientId : null),
+              )
+            : api.getCompleted(
+                request,
+                json.encode(null),
+              );
+
+      // ----- DevTools GA store. -----
+
+      case apiResetDevTools:
+        _devToolsUsage.reset();
+        return api.getCompleted(request, json.encode(true));
+      case apiGetDevToolsFirstRun:
+        // Has DevTools been run first time? To bring up welcome screen.
+        return api.getCompleted(
+          request,
+          json.encode(_devToolsUsage.isFirstRun),
+        );
+      case apiGetDevToolsEnabled:
+        // Is DevTools Analytics collection enabled?
+        return api.getCompleted(request, json.encode(_devToolsUsage.enabled));
+      case apiSetDevToolsEnabled:
+        // Enable or disable DevTools analytics collection.
+        final queryParams = request.requestedUri.queryParameters;
+        if (queryParams.containsKey(devToolsEnabledPropertyName)) {
+          _devToolsUsage.enabled =
+              json.decode(queryParams[devToolsEnabledPropertyName]);
+        }
+        return api.setCompleted(request, json.encode(_devToolsUsage.enabled));
+
+      // ----- DevTools survey store. -----
+
+      case apiSetActiveSurvey:
+        // Assume failure.
+        bool result = false;
+
+        // Set the active survey used to store subsequent apiGetSurveyActionTaken,
+        // apiSetSurveyActionTaken, apiGetSurveyShownCount, and
+        // apiIncrementSurveyShownCount calls.
+        final queryParams = request.requestedUri.queryParameters;
+        if (queryParams.keys.length == 1 &&
+            queryParams.containsKey(activeSurveyName)) {
+          final String theSurveyName = queryParams[activeSurveyName];
+
+          // Set the current activeSurvey.
+          _devToolsUsage.activeSurvey = theSurveyName;
+          result = true;
+        }
+
+        return api.getCompleted(request, json.encode(result));
+      case apiGetSurveyActionTaken:
+        // Request setActiveSurvey has not been requested.
+        if (_devToolsUsage.activeSurvey == null) {
+          return api.badRequest('$errorNoActiveSurvey '
+              '- $apiGetSurveyActionTaken');
+        }
+        // SurveyActionTaken has the survey been acted upon (taken or dismissed)
+        return api.getCompleted(
+          request,
+          json.encode(_devToolsUsage.surveyActionTaken),
+        );
+      // TODO(terry): remove the query param logic for this request.
+      // setSurveyActionTaken should only be called with the value of true, so
+      // we can remove the extra complexity.
+      case apiSetSurveyActionTaken:
+        // Request setActiveSurvey has not been requested.
+        if (_devToolsUsage.activeSurvey == null) {
+          return api.badRequest('$errorNoActiveSurvey '
+              '- $apiSetSurveyActionTaken');
+        }
+        // Set the SurveyActionTaken.
+        // Has the survey been taken or dismissed..
+        final queryParams = request.requestedUri.queryParameters;
+        if (queryParams.containsKey(surveyActionTakenPropertyName)) {
+          _devToolsUsage.surveyActionTaken =
+              json.decode(queryParams[surveyActionTakenPropertyName]);
+        }
+        return api.setCompleted(
+          request,
+          json.encode(_devToolsUsage.surveyActionTaken),
+        );
+      case apiGetSurveyShownCount:
+        // Request setActiveSurvey has not been requested.
+        if (_devToolsUsage.activeSurvey == null) {
+          return api.badRequest('$errorNoActiveSurvey '
+              '- $apiGetSurveyShownCount');
+        }
+        // SurveyShownCount how many times have we asked to take survey.
+        return api.getCompleted(
+          request,
+          json.encode(_devToolsUsage.surveyShownCount),
+        );
+      case apiIncrementSurveyShownCount:
+        // Request setActiveSurvey has not been requested.
+        if (_devToolsUsage.activeSurvey == null) {
+          return api.badRequest('$errorNoActiveSurvey '
+              '- $apiIncrementSurveyShownCount');
+        }
+        // Increment the SurveyShownCount, we've asked about the survey.
+        _devToolsUsage.incrementSurveyShownCount();
+        return api.getCompleted(
+          request,
+          json.encode(_devToolsUsage.surveyShownCount),
+        );
+      case apiGetBaseAppSizeFile:
+        final queryParams = request.requestedUri.queryParameters;
+        if (queryParams.containsKey(baseAppSizeFilePropertyName)) {
+          final filePath = queryParams[baseAppSizeFilePropertyName];
+          final fileJson = LocalFileSystem.devToolsFileAsJson(filePath);
+          if (fileJson == null) {
+            return api.badRequest('No JSON file available at $filePath.');
+          }
+          return api.getCompleted(request, fileJson);
+        }
+        return api.badRequest('Request for base app size file does not '
+            'contain a query parameter with the expected key: '
+            '$baseAppSizeFilePropertyName');
+      case apiGetTestAppSizeFile:
+        final queryParams = request.requestedUri.queryParameters;
+        if (queryParams.containsKey(testAppSizeFilePropertyName)) {
+          final filePath = queryParams[testAppSizeFilePropertyName];
+          final fileJson = LocalFileSystem.devToolsFileAsJson(filePath);
+          if (fileJson == null) {
+            return api.badRequest('No JSON file available at $filePath.');
+          }
+          return api.getCompleted(request, fileJson);
+        }
+        return api.badRequest('Request for test app size file does not '
+            'contain a query parameter with the expected key: '
+            '$testAppSizeFilePropertyName');
+      default:
+        return api.notImplemented(request);
+    }
+  }
+
+  // Accessing Flutter usage file e.g., ~/.flutter.
+  // NOTE: Only access the file if it exists otherwise Flutter Tool hasn't yet
+  //       been run.
+  static final FlutterUsage _usage =
+      FlutterUsage.doesStoreExist ? FlutterUsage() : null;
+
+  // Accessing DevTools usage file e.g., ~/.devtools
+  static final DevToolsUsage _devToolsUsage = DevToolsUsage();
+
+  static DevToolsUsage get devToolsPreferences => _devToolsUsage;
+
+  /// Logs a page view in the DevTools server.
+  ///
+  /// In the open-source version of DevTools, Google Analytics handles this
+  /// without any need to involve the server.
+  FutureOr<shelf.Response> logScreenView(shelf.Request request) =>
+      notImplemented(request);
+
+  /// Return the value of the property.
+  FutureOr<shelf.Response> getCompleted(shelf.Request request, String value) =>
+      shelf.Response.ok('$value');
+
+  /// Return the value of the property after the property value has been set.
+  FutureOr<shelf.Response> setCompleted(shelf.Request request, String value) =>
+      shelf.Response.ok('$value');
+
+  /// A [shelf.Response] for API calls that encountered a request problem e.g.,
+  /// setActiveSurvey not called.
+  ///
+  /// This is a 400 Bad Request response.
+  FutureOr<shelf.Response> badRequest([String logError]) {
+    if (logError != null) print(logError);
+    return shelf.Response(HttpStatus.badRequest);
+  }
+
+  /// A [shelf.Response] for API calls that have not been implemented in this
+  /// server.
+  ///
+  /// This is a no-op 204 No Content response because returning 404 Not Found
+  /// creates unnecessary noise in the console.
+  FutureOr<shelf.Response> notImplemented(shelf.Request request) =>
+      shelf.Response(HttpStatus.noContent);
+}
diff --git a/pkg/dds/lib/src/devtools/usage.dart b/pkg/dds/lib/src/devtools/usage.dart
new file mode 100644
index 0000000..afa35d7
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/usage.dart
@@ -0,0 +1,236 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart=2.9
+
+// TODO(bkonyi): remove once package:devtools_server_api is available
+// See https://github.com/flutter/devtools/issues/2958.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:usage/usage_io.dart';
+
+import 'file_system.dart';
+
+/// Access the file '~/.flutter'.
+class FlutterUsage {
+  /// Create a new Usage instance; [versionOverride] and [configDirOverride] are
+  /// used for testing.
+  FlutterUsage({
+    String settingsName = 'flutter',
+    String versionOverride,
+    String configDirOverride,
+  }) {
+    _analytics = AnalyticsIO('', settingsName, '');
+  }
+
+  Analytics _analytics;
+
+  /// Does the .flutter store exist?
+  static bool get doesStoreExist {
+    return LocalFileSystem.flutterStoreExists();
+  }
+
+  bool get isFirstRun => _analytics.firstRun;
+
+  bool get enabled => _analytics.enabled;
+
+  set enabled(bool value) => _analytics.enabled = value;
+
+  String get clientId => _analytics.clientId;
+}
+
+// Access the DevTools on disk store (~/.devtools/.devtools).
+class DevToolsUsage {
+  /// Create a new Usage instance; [versionOverride] and [configDirOverride] are
+  /// used for testing.
+  DevToolsUsage({
+    String versionOverride,
+    String configDirOverride,
+  }) {
+    LocalFileSystem.maybeMoveLegacyDevToolsStore();
+    properties = IOPersistentProperties(
+      storeName,
+      documentDirPath: LocalFileSystem.devToolsDir(),
+    );
+  }
+
+  static const storeName = '.devtools';
+
+  /// The activeSurvey is the property name of a top-level property
+  /// existing or created in the file ~/.devtools
+  /// If the property doesn't exist it is created with default survey values:
+  ///
+  ///   properties[activeSurvey]['surveyActionTaken'] = false;
+  ///   properties[activeSurvey]['surveyShownCount'] = 0;
+  ///
+  /// It is a requirement that the API apiSetActiveSurvey must be called before
+  /// calling any survey method on DevToolsUsage (addSurvey, rewriteActiveSurvey,
+  /// surveyShownCount, incrementSurveyShownCount, or surveyActionTaken).
+  String _activeSurvey;
+
+  IOPersistentProperties properties;
+
+  static const _surveyActionTaken = 'surveyActionTaken';
+  static const _surveyShownCount = 'surveyShownCount';
+
+  void reset() {
+    properties.remove('firstRun');
+    properties['enabled'] = false;
+  }
+
+  bool get isFirstRun {
+    properties['firstRun'] = properties['firstRun'] == null;
+    return properties['firstRun'];
+  }
+
+  bool get enabled {
+    if (properties['enabled'] == null) {
+      properties['enabled'] = false;
+    }
+
+    return properties['enabled'];
+  }
+
+  set enabled(bool value) {
+    properties['enabled'] = value;
+    return properties['enabled'];
+  }
+
+  bool surveyNameExists(String surveyName) => properties[surveyName] != null;
+
+  void _addSurvey(String surveyName) {
+    assert(activeSurvey != null);
+    assert(activeSurvey == surveyName);
+    rewriteActiveSurvey(false, 0);
+  }
+
+  String get activeSurvey => _activeSurvey;
+
+  set activeSurvey(String surveyName) {
+    assert(surveyName != null);
+    _activeSurvey = surveyName;
+
+    if (!surveyNameExists(activeSurvey)) {
+      // Create the survey if property is non-existent in ~/.devtools
+      _addSurvey(activeSurvey);
+    }
+  }
+
+  /// Need to rewrite the entire survey structure for property to be persisted.
+  void rewriteActiveSurvey(bool actionTaken, int shownCount) {
+    assert(activeSurvey != null);
+    properties[activeSurvey] = {
+      _surveyActionTaken: actionTaken,
+      _surveyShownCount: shownCount,
+    };
+  }
+
+  int get surveyShownCount {
+    assert(activeSurvey != null);
+    final prop = properties[activeSurvey];
+    if (prop[_surveyShownCount] == null) {
+      rewriteActiveSurvey(prop[_surveyActionTaken], 0);
+    }
+    return properties[activeSurvey][_surveyShownCount];
+  }
+
+  void incrementSurveyShownCount() {
+    assert(activeSurvey != null);
+    surveyShownCount; // Ensure surveyShownCount has been initialized.
+    final prop = properties[activeSurvey];
+    rewriteActiveSurvey(prop[_surveyActionTaken], prop[_surveyShownCount] + 1);
+  }
+
+  bool get surveyActionTaken {
+    assert(activeSurvey != null);
+    return properties[activeSurvey][_surveyActionTaken] == true;
+  }
+
+  set surveyActionTaken(bool value) {
+    assert(activeSurvey != null);
+    final prop = properties[activeSurvey];
+    rewriteActiveSurvey(value, prop[_surveyShownCount]);
+  }
+}
+
+abstract class PersistentProperties {
+  PersistentProperties(this.name);
+
+  final String name;
+
+  dynamic operator [](String key);
+
+  void operator []=(String key, dynamic value);
+
+  /// Re-read settings from the backing store.
+  ///
+  /// May be a no-op on some platforms.
+  void syncSettings();
+}
+
+const JsonEncoder _jsonEncoder = JsonEncoder.withIndent('  ');
+
+class IOPersistentProperties extends PersistentProperties {
+  IOPersistentProperties(
+    String name, {
+    String documentDirPath,
+  }) : super(name) {
+    final String fileName = name.replaceAll(' ', '_');
+    documentDirPath ??= LocalFileSystem.devToolsDir();
+    _file = File(path.join(documentDirPath, fileName));
+    if (!_file.existsSync()) {
+      _file.createSync(recursive: true);
+    }
+    syncSettings();
+  }
+
+  IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) {
+    _file = file;
+    if (!_file.existsSync()) {
+      _file.createSync(recursive: true);
+    }
+    syncSettings();
+  }
+
+  File _file;
+
+  Map _map;
+
+  @override
+  dynamic operator [](String key) => _map[key];
+
+  @override
+  void operator []=(String key, dynamic value) {
+    if (value == null && !_map.containsKey(key)) return;
+    if (_map[key] == value) return;
+
+    if (value == null) {
+      _map.remove(key);
+    } else {
+      _map[key] = value;
+    }
+
+    try {
+      _file.writeAsStringSync(_jsonEncoder.convert(_map) + '\n');
+    } catch (_) {}
+  }
+
+  @override
+  void syncSettings() {
+    try {
+      String contents = _file.readAsStringSync();
+      if (contents.isEmpty) contents = '{}';
+      _map = jsonDecode(contents);
+    } catch (_) {
+      _map = {};
+    }
+  }
+
+  void remove(String propertyName) {
+    _map.remove(propertyName);
+  }
+}
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index 221e3df..a69236c 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -3,7 +3,7 @@
   A library used to spawn the Dart Developer Service, used to communicate with
   a Dart VM Service instance.
 
-version: 1.7.6
+version: 1.8.0-dev
 
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/dds
 
@@ -12,18 +12,21 @@
 
 dependencies:
   async: ^2.4.1
+  devtools_shared: ^2.0.0
   json_rpc_2: ^2.2.0
   meta: ^1.1.8
+  path: ^1.8.0
   pedantic: ^1.7.0
   shelf: ^1.0.0
   shelf_proxy: ^1.0.0
+  shelf_static: ^1.0.0-dev
   shelf_web_socket: ^1.0.0
   sse: ^3.7.0
   stream_channel: ^2.0.0
+  usage: ^4.0.0
   vm_service: ^6.0.1-nullsafety.0
   web_socket_channel: ^2.0.0
 
 dev_dependencies:
-  shelf_static: ^1.0.0
   test: ^1.0.0
   webdriver: ^3.0.0
diff --git a/pkg/dds/test/devtools_observatory_connection_test.dart b/pkg/dds/test/devtools_observatory_connection_test.dart
new file mode 100644
index 0000000..3dcc6cc
--- /dev/null
+++ b/pkg/dds/test/devtools_observatory_connection_test.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart=2.10
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:dds/dds.dart';
+
+import 'package:test/test.dart';
+import 'common/test_helper.dart';
+
+// Regression test for https://github.com/dart-lang/sdk/issues/45933.
+
+void main() {
+  Process process;
+  DartDevelopmentService dds;
+
+  setUp(() async {
+    // We don't care what's actually running in the target process for this
+    // test, so we're just using an existing one that invokes `debugger()` so
+    // we know it won't exit before we can connect.
+    process = await spawnDartProcess(
+      'get_stream_history_script.dart',
+      pauseOnStart: false,
+    );
+  });
+
+  tearDown(() async {
+    await dds?.shutdown();
+    process?.kill();
+    dds = null;
+    process = null;
+  });
+
+  defineTest({bool authCodesEnabled}) {
+    test(
+        'Ensure Observatory and DevTools assets are available with '
+        '${authCodesEnabled ? '' : 'no'} auth codes', () async {
+      dds = await DartDevelopmentService.startDartDevelopmentService(
+        remoteVmServiceUri,
+        devToolsConfiguration: DevToolsConfiguration(
+          enable: true,
+          customBuildDirectoryPath: Platform.script.resolve(
+            '../../../third_party/devtools/web',
+          ),
+        ),
+      );
+      expect(dds.isRunning, true);
+
+      final client = HttpClient();
+
+      // Check that Observatory assets are accessible.
+      final observatoryRequest = await client.getUrl(dds.uri);
+      final observatoryResponse = await observatoryRequest.close();
+      expect(observatoryResponse.statusCode, 200);
+      final observatoryContent =
+          await observatoryResponse.transform(utf8.decoder).join();
+      expect(observatoryContent, startsWith('<!DOCTYPE html>'));
+
+      // Check that DevTools assets are accessible.
+      final devtoolsRequest = await client.getUrl(dds.devToolsUri);
+      final devtoolsResponse = await devtoolsRequest.close();
+      expect(devtoolsResponse.statusCode, 200);
+      final devtoolsContent =
+          await devtoolsResponse.transform(utf8.decoder).join();
+      expect(devtoolsContent, startsWith('<!DOCTYPE html>'));
+    });
+  }
+
+  defineTest(authCodesEnabled: true);
+  defineTest(authCodesEnabled: false);
+}
diff --git a/pkg/vm_service/CHANGELOG.md b/pkg/vm_service/CHANGELOG.md
index 5f0da4d..47f3893 100644
--- a/pkg/vm_service/CHANGELOG.md
+++ b/pkg/vm_service/CHANGELOG.md
@@ -1,6 +1,6 @@
 # Changelog
-
-## 6.3.0-dev
+## 7.0.0
+- *breaking bug fix*: Fixed issue where response parsing could fail for `Context`.
 - Add support for `setBreakpointState` RPC and updated `Breakpoint` class to include
   `enabled` property.
 
diff --git a/pkg/vm_service/lib/src/vm_service.dart b/pkg/vm_service/lib/src/vm_service.dart
index 50fe029..a42219a 100644
--- a/pkg/vm_service/lib/src/vm_service.dart
+++ b/pkg/vm_service/lib/src/vm_service.dart
@@ -3318,7 +3318,7 @@
 
   /// The enclosing context for this context.
   @optional
-  Context? parent;
+  ContextRef? parent;
 
   /// The variables in this context object.
   List<ContextElement>? variables;
@@ -3334,7 +3334,8 @@
 
   Context._fromJson(Map<String, dynamic> json) : super._fromJson(json) {
     length = json['length'] ?? -1;
-    parent = createServiceObject(json['parent'], const ['Context']) as Context?;
+    parent = createServiceObject(json['parent'], const ['ContextRef'])
+        as ContextRef?;
     variables = List<ContextElement>.from(
         createServiceObject(json['variables'], const ['ContextElement'])
                 as List? ??
diff --git a/pkg/vm_service/pubspec.yaml b/pkg/vm_service/pubspec.yaml
index dfe2e30..a7db2e8 100644
--- a/pkg/vm_service/pubspec.yaml
+++ b/pkg/vm_service/pubspec.yaml
@@ -3,7 +3,7 @@
   A library to communicate with a service implementing the Dart VM
   service protocol.
 
-version: 6.3.0-dev
+version: 7.0.0
 
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/vm_service
 
diff --git a/runtime/bin/main.cc b/runtime/bin/main.cc
index ee35109..95c7166 100644
--- a/runtime/bin/main.cc
+++ b/runtime/bin/main.cc
@@ -553,13 +553,12 @@
     vm_service_server_port = 0;
   }
 
-  // We do not want to wait for DDS to advertise availability of VM service in the
-  // following scenarios:
-  // - When the VM service is disabled (can be started at a later time via SIGQUIT).
-  // - The DartDev CLI is disabled (CLI isolate starts DDS) and VM service is enabled.
-  bool wait_for_dds_to_advertise_service =
-      !Options::disable_dart_dev() && Options::enable_vm_service();
-
+  // We do not want to wait for DDS to advertise availability of VM service in
+  // the following scenarios:
+  // - The DartDev CLI is disabled (CLI isolate starts DDS) and VM service is
+  //   enabled.
+  // TODO(bkonyi): do we want to tie DevTools / DDS to the CLI in the long run?
+  bool wait_for_dds_to_advertise_service = !Options::disable_dart_dev();
   // Load embedder specific bits and return.
   if (!VmService::Setup(
           Options::disable_dart_dev() ? Options::vm_service_server_ip()
diff --git a/runtime/bin/main_options.cc b/runtime/bin/main_options.cc
index b002ab8..55fef46 100644
--- a/runtime/bin/main_options.cc
+++ b/runtime/bin/main_options.cc
@@ -585,7 +585,7 @@
         run_command = true;
       }
       if (!Options::disable_dart_dev() && enable_vm_service_ && run_command) {
-        const char* dds_format_str = "--launch-dds=%s:%d";
+        const char* dds_format_str = "--launch-dds=%s\\:%d";
         size_t size =
             snprintf(nullptr, 0, dds_format_str, vm_service_server_ip(),
                      vm_service_server_port());
@@ -605,6 +605,7 @@
       first_option = false;
     }
   }
+
   // Verify consistency of arguments.
 
   // snapshot_depfile is an alias for depfile. Passing them both is an error.
diff --git a/runtime/tests/vm/dart/isolates/concurrency_stress_sanity_test.dart b/runtime/tests/vm/dart/isolates/concurrency_stress_sanity_test.dart
index 1e2860b..6334701 100644
--- a/runtime/tests/vm/dart/isolates/concurrency_stress_sanity_test.dart
+++ b/runtime/tests/vm/dart/isolates/concurrency_stress_sanity_test.dart
@@ -20,7 +20,7 @@
 // fuzzing test).
 main() async {
   if (!Platform.isLinux) return;
-  if (!Platform.executable.contains("ReleaseX64/dart")) return;
+  if (!Platform.executable.endsWith("ReleaseX64/dart")) return;
 
   final dartExecutable = Platform.executable;
   await withTempDir((String tempDir) async {
diff --git a/runtime/tests/vm/dart_2/isolates/concurrency_stress_sanity_test.dart b/runtime/tests/vm/dart_2/isolates/concurrency_stress_sanity_test.dart
index a68084f..3af1a18 100644
--- a/runtime/tests/vm/dart_2/isolates/concurrency_stress_sanity_test.dart
+++ b/runtime/tests/vm/dart_2/isolates/concurrency_stress_sanity_test.dart
@@ -20,7 +20,7 @@
 // fuzzing test).
 main() async {
   if (!Platform.isLinux) return;
-  if (!Platform.executable.contains("ReleaseX64/dart")) return;
+  if (!Platform.executable.endsWith("ReleaseX64/dart")) return;
 
   final dartExecutable = Platform.executable;
   await withTempDir((String tempDir) async {
diff --git a/runtime/vm/compiler/assembler/assembler_arm.cc b/runtime/vm/compiler/assembler/assembler_arm.cc
index bf1dc6a..86f3c78 100644
--- a/runtime/vm/compiler/assembler/assembler_arm.cc
+++ b/runtime/vm/compiler/assembler/assembler_arm.cc
@@ -2048,6 +2048,20 @@
   SmiTag(result);
 }
 
+void Assembler::EnsureHasClassIdInDEBUG(intptr_t cid,
+                                        Register src,
+                                        Register scratch) {
+#if defined(DEBUG)
+  Comment("Check that object in register has cid %" Pd "", cid);
+  Label matches;
+  LoadClassIdMayBeSmi(scratch, src);
+  CompareImmediate(scratch, cid);
+  BranchIf(EQUAL, &matches, Assembler::kNearJump);
+  Breakpoint();
+  Bind(&matches);
+#endif
+}
+
 void Assembler::BailoutIfInvalidBranchOffset(int32_t offset) {
   if (!CanEncodeBranchDistance(offset)) {
     ASSERT(!use_far_branches());
diff --git a/runtime/vm/compiler/assembler/assembler_arm.h b/runtime/vm/compiler/assembler/assembler_arm.h
index e03aa97..6822e73 100644
--- a/runtime/vm/compiler/assembler/assembler_arm.h
+++ b/runtime/vm/compiler/assembler/assembler_arm.h
@@ -918,6 +918,9 @@
   void CompareClassId(Register object, intptr_t class_id, Register scratch);
   void LoadClassIdMayBeSmi(Register result, Register object);
   void LoadTaggedClassIdMayBeSmi(Register result, Register object);
+  void EnsureHasClassIdInDEBUG(intptr_t cid,
+                               Register src,
+                               Register scratch) override;
 
   intptr_t FindImmediate(int32_t imm);
   bool CanLoadFromObjectPool(const Object& object) const;
diff --git a/runtime/vm/compiler/assembler/assembler_arm64.cc b/runtime/vm/compiler/assembler/assembler_arm64.cc
index a1f7c7f..7ef550b 100644
--- a/runtime/vm/compiler/assembler/assembler_arm64.cc
+++ b/runtime/vm/compiler/assembler/assembler_arm64.cc
@@ -1325,6 +1325,20 @@
   }
 }
 
+void Assembler::EnsureHasClassIdInDEBUG(intptr_t cid,
+                                        Register src,
+                                        Register scratch) {
+#if defined(DEBUG)
+  Comment("Check that object in register has cid %" Pd "", cid);
+  Label matches;
+  LoadClassIdMayBeSmi(scratch, src);
+  CompareImmediate(scratch, cid);
+  BranchIf(EQUAL, &matches, Assembler::kNearJump);
+  Breakpoint();
+  Bind(&matches);
+#endif
+}
+
 // Frame entry and exit.
 void Assembler::ReserveAlignedFrameSpace(intptr_t frame_space) {
   // Reserve space for arguments and align frame before entering
diff --git a/runtime/vm/compiler/assembler/assembler_arm64.h b/runtime/vm/compiler/assembler/assembler_arm64.h
index 4dcf55a..ef935b4 100644
--- a/runtime/vm/compiler/assembler/assembler_arm64.h
+++ b/runtime/vm/compiler/assembler/assembler_arm64.h
@@ -1887,6 +1887,9 @@
   // Note: input and output registers must be different.
   void LoadClassIdMayBeSmi(Register result, Register object);
   void LoadTaggedClassIdMayBeSmi(Register result, Register object);
+  void EnsureHasClassIdInDEBUG(intptr_t cid,
+                               Register src,
+                               Register scratch) override;
 
   // Reserve specifies how much space to reserve for the Dart stack.
   void SetupDartSP(intptr_t reserve = 4096);
diff --git a/runtime/vm/compiler/assembler/assembler_base.h b/runtime/vm/compiler/assembler/assembler_base.h
index 383a067..5b0c4b0 100644
--- a/runtime/vm/compiler/assembler/assembler_base.h
+++ b/runtime/vm/compiler/assembler/assembler_base.h
@@ -655,6 +655,10 @@
   }
 #endif  // defined(DART_COMPRESSED_POINTERS)
 
+  virtual void EnsureHasClassIdInDEBUG(intptr_t cid,
+                                       Register src,
+                                       Register scratch) = 0;
+
   intptr_t InsertAlignedRelocation(BSS::Relocation reloc);
 
   void Unimplemented(const char* message);
diff --git a/runtime/vm/compiler/assembler/assembler_ia32.cc b/runtime/vm/compiler/assembler/assembler_ia32.cc
index 4850162..2d0c3fb 100644
--- a/runtime/vm/compiler/assembler/assembler_ia32.cc
+++ b/runtime/vm/compiler/assembler/assembler_ia32.cc
@@ -2825,6 +2825,20 @@
   }
 }
 
+void Assembler::EnsureHasClassIdInDEBUG(intptr_t cid,
+                                        Register src,
+                                        Register scratch) {
+#if defined(DEBUG)
+  Comment("Check that object in register has cid %" Pd "", cid);
+  Label matches;
+  LoadClassIdMayBeSmi(scratch, src);
+  CompareImmediate(scratch, cid);
+  BranchIf(EQUAL, &matches, Assembler::kNearJump);
+  Breakpoint();
+  Bind(&matches);
+#endif
+}
+
 Address Assembler::ElementAddressForIntIndex(bool is_external,
                                              intptr_t cid,
                                              intptr_t index_scale,
diff --git a/runtime/vm/compiler/assembler/assembler_ia32.h b/runtime/vm/compiler/assembler/assembler_ia32.h
index 5447918..2c75037 100644
--- a/runtime/vm/compiler/assembler/assembler_ia32.h
+++ b/runtime/vm/compiler/assembler/assembler_ia32.h
@@ -829,6 +829,9 @@
 
   void LoadClassIdMayBeSmi(Register result, Register object);
   void LoadTaggedClassIdMayBeSmi(Register result, Register object);
+  void EnsureHasClassIdInDEBUG(intptr_t cid,
+                               Register src,
+                               Register scratch) override;
 
   void SmiUntagOrCheckClass(Register object,
                             intptr_t class_id,
diff --git a/runtime/vm/compiler/assembler/assembler_x64.cc b/runtime/vm/compiler/assembler/assembler_x64.cc
index 7ab5c8e..72d30fd 100644
--- a/runtime/vm/compiler/assembler/assembler_x64.cc
+++ b/runtime/vm/compiler/assembler/assembler_x64.cc
@@ -2405,6 +2405,20 @@
   }
 }
 
+void Assembler::EnsureHasClassIdInDEBUG(intptr_t cid,
+                                        Register src,
+                                        Register scratch) {
+#if defined(DEBUG)
+  Comment("Check that object in register has cid %" Pd "", cid);
+  Label matches;
+  LoadClassIdMayBeSmi(scratch, src);
+  CompareImmediate(scratch, cid);
+  BranchIf(EQUAL, &matches, Assembler::kNearJump);
+  Breakpoint();
+  Bind(&matches);
+#endif
+}
+
 Address Assembler::VMTagAddress() {
   return Address(THR, target::Thread::vm_tag_offset());
 }
diff --git a/runtime/vm/compiler/assembler/assembler_x64.h b/runtime/vm/compiler/assembler/assembler_x64.h
index 5acae47..4bc8495 100644
--- a/runtime/vm/compiler/assembler/assembler_x64.h
+++ b/runtime/vm/compiler/assembler/assembler_x64.h
@@ -883,6 +883,10 @@
   void LoadClassIdMayBeSmi(Register result, Register object);
   void LoadTaggedClassIdMayBeSmi(Register result, Register object);
 
+  void EnsureHasClassIdInDEBUG(intptr_t cid,
+                               Register src,
+                               Register scratch) override;
+
   // CheckClassIs fused with optimistic SmiUntag.
   // Value in the register object is untagged optimistically.
   void SmiUntagOrCheckClass(Register object, intptr_t class_id, Label* smi);
diff --git a/runtime/vm/compiler/backend/il.cc b/runtime/vm/compiler/backend/il.cc
index 226e449..4e02ded 100644
--- a/runtime/vm/compiler/backend/il.cc
+++ b/runtime/vm/compiler/backend/il.cc
@@ -993,10 +993,12 @@
 
 LocationSummary* AllocateClosureInstr::MakeLocationSummary(Zone* zone,
                                                            bool opt) const {
-  const intptr_t kNumInputs = 0;
+  const intptr_t kNumInputs = inputs_.length();
   const intptr_t kNumTemps = 0;
   LocationSummary* locs = new (zone)
       LocationSummary(zone, kNumInputs, kNumTemps, LocationSummary::kCall);
+  locs->set_in(kFunctionPos,
+               Location::RegisterLocation(AllocateClosureABI::kFunctionReg));
   locs->set_out(0, Location::RegisterLocation(AllocateClosureABI::kResultReg));
   return locs;
 }
diff --git a/runtime/vm/compiler/backend/il.h b/runtime/vm/compiler/backend/il.h
index 917d7df..ce21bc8 100644
--- a/runtime/vm/compiler/backend/il.h
+++ b/runtime/vm/compiler/backend/il.h
@@ -6207,21 +6207,31 @@
   DISALLOW_COPY_AND_ASSIGN(AllocateObjectInstr);
 };
 
-// Allocates and null initializes a closure object. The closure function, when
-// non-null, is used to determine the precise type of the resulting closure
-// and to inline the closure function when applicable.
-class AllocateClosureInstr : public TemplateAllocation<0> {
+// Allocates and null initializes a closure object, given the closure function
+// as a value.
+class AllocateClosureInstr : public TemplateAllocation<1> {
  public:
+  enum Inputs { kFunctionPos = 0 };
   AllocateClosureInstr(const InstructionSource& source,
-                       const Function& closure_function,
+                       Value* closure_function,
                        intptr_t deopt_id)
-      : TemplateAllocation(source, deopt_id),
-        closure_function_(closure_function) {}
+      : TemplateAllocation(source, deopt_id) {
+    SetInputAt(kFunctionPos, closure_function);
+  }
 
   DECLARE_INSTRUCTION(AllocateClosure)
   virtual CompileType ComputeType() const;
 
-  const Function& closure_function() const { return closure_function_; }
+  Value* closure_function() const { return inputs_[kFunctionPos]; }
+
+  const Function& known_function() const {
+    Value* const value = closure_function();
+    if (value->BindsToConstant()) {
+      ASSERT(value->BoundConstant().IsFunction());
+      return Function::Cast(value->BoundConstant());
+    }
+    return Object::null_function();
+  }
 
   virtual bool HasUnknownSideEffects() const { return false; }
 
@@ -6230,11 +6240,7 @@
         compiler::target::Closure::InstanceSize());
   }
 
-  PRINT_OPERANDS_TO_SUPPORT
-
  private:
-  const Function& closure_function_;
-
   DISALLOW_COPY_AND_ASSIGN(AllocateClosureInstr);
 };
 
diff --git a/runtime/vm/compiler/backend/il_printer.cc b/runtime/vm/compiler/backend/il_printer.cc
index c816dac..2dcf90c 100644
--- a/runtime/vm/compiler/backend/il_printer.cc
+++ b/runtime/vm/compiler/backend/il_printer.cc
@@ -672,14 +672,6 @@
   AllocationInstr::PrintOperandsTo(f);
 }
 
-void AllocateClosureInstr::PrintOperandsTo(BaseTextBuffer* f) const {
-  f->Printf("function=%s", closure_function().ToCString());
-  if (InputCount() > 0 || Identity().IsNotAliased()) {
-    f->AddString(", ");
-  }
-  TemplateAllocation::PrintOperandsTo(f);
-}
-
 void MaterializeObjectInstr::PrintOperandsTo(BaseTextBuffer* f) const {
   f->Printf("%s", String::Handle(cls_.ScrubbedName()).ToCString());
   for (intptr_t i = 0; i < InputCount(); i++) {
diff --git a/runtime/vm/compiler/backend/il_test.cc b/runtime/vm/compiler/backend/il_test.cc
index 3a0286c..3f70e07 100644
--- a/runtime/vm/compiler/backend/il_test.cc
+++ b/runtime/vm/compiler/backend/il_test.cc
@@ -178,13 +178,13 @@
   std::vector<const char*> expected_stores_jit;
   std::vector<const char*> expected_stores_aot;
 
-  expected_stores_jit.insert(expected_stores_jit.end(),
-                             {"value", "Context.parent", "Context.parent",
-                              "value", "Closure.function_type_arguments",
-                              "Closure.function", "Closure.context"});
-  expected_stores_aot.insert(expected_stores_aot.end(),
-                             {"value", "Closure.function_type_arguments",
-                              "Closure.function", "Closure.context"});
+  expected_stores_jit.insert(
+      expected_stores_jit.end(),
+      {"value", "Context.parent", "Context.parent", "value",
+       "Closure.function_type_arguments", "Closure.context"});
+  expected_stores_aot.insert(
+      expected_stores_aot.end(),
+      {"value", "Closure.function_type_arguments", "Closure.context"});
 
   RunInitializingStoresTest(root_library, "f4", CompilerPass::kJIT,
                             expected_stores_jit);
diff --git a/runtime/vm/compiler/backend/inliner.cc b/runtime/vm/compiler/backend/inliner.cc
index c727f66..62172d2 100644
--- a/runtime/vm/compiler/backend/inliner.cc
+++ b/runtime/vm/compiler/backend/inliner.cc
@@ -1513,7 +1513,7 @@
       Definition* receiver =
           call->Receiver()->definition()->OriginalDefinition();
       if (const auto* alloc = receiver->AsAllocateClosure()) {
-        target = alloc->closure_function().ptr();
+        target = alloc->known_function().ptr();
       } else if (ConstantInstr* constant = receiver->AsConstant()) {
         if (constant->value().IsClosure()) {
           target = Closure::Cast(constant->value()).function();
diff --git a/runtime/vm/compiler/backend/redundancy_elimination.cc b/runtime/vm/compiler/backend/redundancy_elimination.cc
index bf2afc0..7b27daf 100644
--- a/runtime/vm/compiler/backend/redundancy_elimination.cc
+++ b/runtime/vm/compiler/backend/redundancy_elimination.cc
@@ -2087,6 +2087,10 @@
               // explicitly initializes each non-null closure field in the flow
               // graph with StoreInstanceField instructions post-allocation.
               Definition* forward_def = graph_->constant_null();
+              // Forward values passed as AllocateClosureInstr inputs.
+              if (slot->IsIdentical(Slot::Closure_function())) {
+                forward_def = alloc->closure_function()->definition();
+              }
               gen->Add(place_id);
               if (out_values == nullptr) out_values = CreateBlockOutValues();
               (*out_values)[place_id] = forward_def;
@@ -3727,8 +3731,10 @@
     }
   }
   if (auto alloc_closure = alloc->AsAllocateClosure()) {
-    // Any closure slots that are non-null are explicitly initialized
-    // post-allocation using StoreInstanceField instructions.
+    // Add slots for any instruction inputs. Any closure slots not listed below
+    // that are non-null are explicitly initialized post-allocation using
+    // StoreInstanceField instructions.
+    AddSlot(slots, Slot::Closure_function());
   }
   if (alloc->IsCreateArray()) {
     AddSlot(
diff --git a/runtime/vm/compiler/backend/type_propagator.cc b/runtime/vm/compiler/backend/type_propagator.cc
index 72a427f..554ea11 100644
--- a/runtime/vm/compiler/backend/type_propagator.cc
+++ b/runtime/vm/compiler/backend/type_propagator.cc
@@ -1491,7 +1491,7 @@
 }
 
 CompileType AllocateClosureInstr::ComputeType() const {
-  const auto& func = closure_function();
+  const auto& func = known_function();
   if (!func.IsNull()) {
     const auto& sig = FunctionType::ZoneHandle(func.signature());
     return CompileType(CompileType::kNonNullable, kClosureCid, &sig);
diff --git a/runtime/vm/compiler/frontend/base_flow_graph_builder.cc b/runtime/vm/compiler/frontend/base_flow_graph_builder.cc
index 8274032..acfc656 100644
--- a/runtime/vm/compiler/frontend/base_flow_graph_builder.cc
+++ b/runtime/vm/compiler/frontend/base_flow_graph_builder.cc
@@ -889,10 +889,10 @@
   return Fragment(allocate);
 }
 
-Fragment BaseFlowGraphBuilder::AllocateClosure(const Function& closure_function,
-                                               TokenPosition position) {
-  auto* allocate = new (Z) AllocateClosureInstr(
-      InstructionSource(position), closure_function, GetNextDeoptId());
+Fragment BaseFlowGraphBuilder::AllocateClosure(TokenPosition position) {
+  auto const function = Pop();
+  auto* allocate = new (Z) AllocateClosureInstr(InstructionSource(position),
+                                                function, GetNextDeoptId());
   Push(allocate);
   return Fragment(allocate);
 }
@@ -1006,7 +1006,8 @@
   code += LoadLocal(pointer);
   code += StoreNativeField(*context_slots[0]);
 
-  code += AllocateClosure(target);
+  code += Constant(target);
+  code += AllocateClosure();
   LocalVariable* closure = MakeTemporary();
 
   code += LoadLocal(closure);
@@ -1014,11 +1015,6 @@
   code += StoreNativeField(Slot::Closure_context(),
                            StoreInstanceFieldInstr::Kind::kInitializing);
 
-  code += LoadLocal(closure);
-  code += Constant(target);
-  code += StoreNativeField(Slot::Closure_function(),
-                           StoreInstanceFieldInstr::Kind::kInitializing);
-
   // Drop address and context.
   code += DropTempsPreserveTop(2);
 
diff --git a/runtime/vm/compiler/frontend/base_flow_graph_builder.h b/runtime/vm/compiler/frontend/base_flow_graph_builder.h
index 483750a..e3948fc 100644
--- a/runtime/vm/compiler/frontend/base_flow_graph_builder.h
+++ b/runtime/vm/compiler/frontend/base_flow_graph_builder.h
@@ -341,10 +341,8 @@
   Fragment AssertBool(TokenPosition position);
   Fragment BooleanNegate();
   Fragment AllocateContext(const ZoneGrowableArray<const Slot*>& scope);
-  // closure_function can be null if not statically known (i.e., copying from
-  // another closure object).
-  Fragment AllocateClosure(const Function& closure_function,
-                           TokenPosition position = TokenPosition::kNoSource);
+  // Top of the stack should be the closure function.
+  Fragment AllocateClosure(TokenPosition position = TokenPosition::kNoSource);
   Fragment CreateArray();
   Fragment AllocateTypedData(TokenPosition position, classid_t class_id);
   Fragment InstantiateType(const AbstractType& type);
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
index 36e5460..1027098 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
@@ -4374,8 +4374,11 @@
   Fragment instructions = BuildExpression();
   LocalVariable* original_closure = MakeTemporary();
 
-  // The closure function isn't known at compile time.
-  instructions += flow_graph_builder_->AllocateClosure(Object::null_function());
+  // Load the target function and allocate the closure.
+  instructions += LoadLocal(original_closure);
+  instructions +=
+      flow_graph_builder_->LoadNativeField(Slot::Closure_function());
+  instructions += flow_graph_builder_->AllocateClosure();
   LocalVariable* new_closure = MakeTemporary();
 
   intptr_t num_type_args = ReadListLength();
@@ -4406,14 +4409,6 @@
       StoreInstanceFieldInstr::Kind::kInitializing);
   instructions += DropTemporary(&type_args_vec);
 
-  // Copy over the target function.
-  instructions += LoadLocal(new_closure);
-  instructions += LoadLocal(original_closure);
-  instructions +=
-      flow_graph_builder_->LoadNativeField(Slot::Closure_function());
-  instructions += flow_graph_builder_->StoreNativeField(
-      Slot::Closure_function(), StoreInstanceFieldInstr::Kind::kInitializing);
-
   // Copy over the instantiator type arguments.
   instructions += LoadLocal(new_closure);
   instructions += LoadLocal(original_closure);
@@ -5568,7 +5563,9 @@
 
   function_node_helper.ReadUntilExcluding(FunctionNodeHelper::kEnd);
 
-  Fragment instructions = flow_graph_builder_->AllocateClosure(function);
+  Fragment instructions;
+  instructions += Constant(function);
+  instructions += flow_graph_builder_->AllocateClosure();
   LocalVariable* closure = MakeTemporary();
 
   // The function signature can have uninstantiated class type parameters.
@@ -5598,12 +5595,7 @@
         StoreInstanceFieldInstr::Kind::kInitializing);
   }
 
-  // Store the function and the context in the closure.
-  instructions += LoadLocal(closure);
-  instructions += Constant(function);
-  instructions += flow_graph_builder_->StoreNativeField(
-      Slot::Closure_function(), StoreInstanceFieldInstr::Kind::kInitializing);
-
+  // Store the context in the closure.
   instructions += LoadLocal(closure);
   instructions += LoadLocal(parsed_function()->current_context_var());
   instructions += flow_graph_builder_->StoreNativeField(
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
index 89105dd..df727ff 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
@@ -209,7 +209,6 @@
   Fragment AllocateObject(TokenPosition position,
                           const Class& klass,
                           intptr_t argument_count);
-  Fragment AllocateObject(const Class& klass, const Function& closure_function);
   Fragment AllocateContext(const ZoneGrowableArray<const Slot*>& context_slots);
   Fragment LoadNativeField(const Slot& field);
   Fragment StoreLocal(TokenPosition position, LocalVariable* variable);
diff --git a/runtime/vm/compiler/frontend/kernel_to_il.cc b/runtime/vm/compiler/frontend/kernel_to_il.cc
index 064f260..88c10b3 100644
--- a/runtime/vm/compiler/frontend/kernel_to_il.cc
+++ b/runtime/vm/compiler/frontend/kernel_to_il.cc
@@ -1584,7 +1584,8 @@
 Fragment FlowGraphBuilder::BuildImplicitClosureCreation(
     const Function& target) {
   Fragment fragment;
-  fragment += AllocateClosure(target);
+  fragment += Constant(target);
+  fragment += AllocateClosure();
   LocalVariable* closure = MakeTemporary();
 
   // The function signature can have uninstantiated class type parameters.
@@ -1605,12 +1606,7 @@
   fragment += AllocateContext(implicit_closure_scope->context_slots());
   LocalVariable* context = MakeTemporary();
 
-  // Store the function and the context in the closure.
-  fragment += LoadLocal(closure);
-  fragment += Constant(target);
-  fragment += StoreNativeField(Slot::Closure_function(),
-                               StoreInstanceFieldInstr::Kind::kInitializing);
-
+  // Store the context in the closure.
   fragment += LoadLocal(closure);
   fragment += LoadLocal(context);
   fragment += StoreNativeField(Slot::Closure_context(),
@@ -1659,7 +1655,7 @@
     return true;
   }
   if (auto const alloc = definition->AsAllocateClosure()) {
-    return !alloc->closure_function().IsNull();
+    return !alloc->known_function().IsNull();
   }
   return definition->IsLoadLocal();
 }
diff --git a/runtime/vm/compiler/stub_code_compiler.cc b/runtime/vm/compiler/stub_code_compiler.cc
index 5af39cf..4dbf99c 100644
--- a/runtime/vm/compiler/stub_code_compiler.cc
+++ b/runtime/vm/compiler/stub_code_compiler.cc
@@ -756,6 +756,8 @@
 #endif  // !defined(TARGET_ARCH_IA32)
 
 // Called for inline allocation of closure.
+// Input (preserved):
+//   AllocateClosureABI::kFunctionReg: closure function.
 // Output:
 //   AllocateClosureABI::kResultReg: new allocated Closure object.
 // Clobbered:
@@ -763,6 +765,8 @@
 void StubCodeCompiler::GenerateAllocateClosureStub(Assembler* assembler) {
   const intptr_t instance_size =
       target::RoundedAllocationSize(target::Closure::InstanceSize());
+  __ EnsureHasClassIdInDEBUG(kFunctionCid, AllocateClosureABI::kFunctionReg,
+                             AllocateClosureABI::kScratchReg);
   if (!FLAG_use_slow_path && FLAG_inline_alloc) {
     Label slow_case;
     __ Comment("Inline allocation of uninitialized closure");
@@ -777,8 +781,10 @@
                          AllocateClosureABI::kScratchReg);
 
     __ Comment("Inline initialization of allocated closure");
-    // Put null in the scratch register for initializing boxed fields.
+    // Put null in the scratch register for initializing most boxed fields.
     // We initialize the fields in offset order below.
+    // Since the TryAllocateObject above did not go to the slow path, we're
+    // guaranteed an object in new space here, and thus no barriers are needed.
     __ LoadObject(AllocateClosureABI::kScratchReg, NullObject());
     __ StoreToSlotNoBarrier(AllocateClosureABI::kScratchReg,
                             AllocateClosureABI::kResultReg,
@@ -789,7 +795,7 @@
     __ StoreToSlotNoBarrier(AllocateClosureABI::kScratchReg,
                             AllocateClosureABI::kResultReg,
                             Slot::Closure_delayed_type_arguments());
-    __ StoreToSlotNoBarrier(AllocateClosureABI::kScratchReg,
+    __ StoreToSlotNoBarrier(AllocateClosureABI::kFunctionReg,
                             AllocateClosureABI::kResultReg,
                             Slot::Closure_function());
     __ StoreToSlotNoBarrier(AllocateClosureABI::kScratchReg,
@@ -808,7 +814,9 @@
   __ Comment("Closure allocation via runtime");
   __ EnterStubFrame();
   __ PushObject(NullObject());  // Space on the stack for the return value.
-  __ CallRuntime(kAllocateClosureRuntimeEntry, 0);
+  __ PushRegister(AllocateClosureABI::kFunctionReg);
+  __ CallRuntime(kAllocateClosureRuntimeEntry, 1);
+  __ PopRegister(AllocateClosureABI::kFunctionReg);
   __ PopRegister(AllocateClosureABI::kResultReg);
   ASSERT(target::WillAllocateNewOrRememberedObject(instance_size));
   EnsureIsNewOrRemembered(assembler, /*preserve_registers=*/false);
diff --git a/runtime/vm/compiler/stub_code_compiler_arm.cc b/runtime/vm/compiler/stub_code_compiler_arm.cc
index e757ee3..ea32cf7 100644
--- a/runtime/vm/compiler/stub_code_compiler_arm.cc
+++ b/runtime/vm/compiler/stub_code_compiler_arm.cc
@@ -236,7 +236,8 @@
   __ ldr(R3, Address(R0, R4), NE);
 
   // Push type arguments & extracted method.
-  __ PushList(1 << R3 | 1 << R1);
+  __ Push(R3);
+  __ Push(R1);
 
   // Allocate context.
   {
@@ -267,32 +268,41 @@
   __ StoreIntoObject(R0, FieldAddress(R0, target::Context::variable_offset(0)),
                      R1);
 
+  // Pop function before pushing context.
+  __ Pop(AllocateClosureABI::kFunctionReg);
+
   // Push context.
   __ Push(R0);
 
-  // Allocate closure.
+  // Allocate closure. After this point, we only use the registers in
+  // AllocateClosureABI.
   __ LoadObject(CODE_REG, closure_allocation_stub);
-  __ ldr(R1, FieldAddress(CODE_REG, target::Code::entry_point_offset(
-                                        CodeEntryKind::kUnchecked)));
-  __ blx(R1);
+  __ ldr(AllocateClosureABI::kScratchReg,
+         FieldAddress(CODE_REG, target::Code::entry_point_offset()));
+  __ blx(AllocateClosureABI::kScratchReg);
 
   // Populate closure object.
-  __ Pop(R1);  // Pop context.
-  __ StoreIntoObject(R0, FieldAddress(R0, target::Closure::context_offset()),
-                     R1);
-  __ PopList(1 << R3 | 1 << R1);  // Pop type arguments & extracted method.
+  __ Pop(AllocateClosureABI::kScratchReg);  // Pop context.
+  __ StoreIntoObject(AllocateClosureABI::kResultReg,
+                     FieldAddress(AllocateClosureABI::kResultReg,
+                                  target::Closure::context_offset()),
+                     AllocateClosureABI::kScratchReg);
+  __ Pop(AllocateClosureABI::kScratchReg);  // Pop type arguments.
   __ StoreIntoObjectNoBarrier(
-      R0, FieldAddress(R0, target::Closure::function_offset()), R1);
+      AllocateClosureABI::kResultReg,
+      FieldAddress(AllocateClosureABI::kResultReg,
+                   target::Closure::instantiator_type_arguments_offset()),
+      AllocateClosureABI::kScratchReg);
+  __ LoadObject(AllocateClosureABI::kScratchReg, EmptyTypeArguments());
   __ StoreIntoObjectNoBarrier(
-      R0,
-      FieldAddress(R0, target::Closure::instantiator_type_arguments_offset()),
-      R3);
-  __ LoadObject(R1, EmptyTypeArguments());
-  __ StoreIntoObjectNoBarrier(
-      R0, FieldAddress(R0, target::Closure::delayed_type_arguments_offset()),
-      R1);
+      AllocateClosureABI::kResultReg,
+      FieldAddress(AllocateClosureABI::kResultReg,
+                   target::Closure::delayed_type_arguments_offset()),
+      AllocateClosureABI::kScratchReg);
 
   __ LeaveStubFrame();
+  // No-op if the two are the same.
+  __ MoveRegister(R0, AllocateClosureABI::kResultReg);
   __ Ret();
 }
 
diff --git a/runtime/vm/compiler/stub_code_compiler_arm64.cc b/runtime/vm/compiler/stub_code_compiler_arm64.cc
index 78627e8..ef37b4e 100644
--- a/runtime/vm/compiler/stub_code_compiler_arm64.cc
+++ b/runtime/vm/compiler/stub_code_compiler_arm64.cc
@@ -473,7 +473,8 @@
   __ Bind(&no_type_args);
 
   // Push type arguments & extracted method.
-  __ PushPair(R3, R1);
+  __ Push(R3);
+  __ Push(R1);
 
   // Allocate context.
   {
@@ -504,32 +505,41 @@
   __ StoreIntoObject(R0, FieldAddress(R0, target::Context::variable_offset(0)),
                      R1);
 
+  // Pop function before pushing context.
+  __ Pop(AllocateClosureABI::kFunctionReg);
+
   // Push context.
   __ Push(R0);
 
-  // Allocate closure.
+  // Allocate closure. After this point, we only use the registers in
+  // AllocateClosureABI.
   __ LoadObject(CODE_REG, closure_allocation_stub);
-  __ ldr(R1, FieldAddress(CODE_REG, target::Code::entry_point_offset(
-                                        CodeEntryKind::kUnchecked)));
-  __ blr(R1);
+  __ ldr(AllocateClosureABI::kScratchReg,
+         FieldAddress(CODE_REG, target::Code::entry_point_offset()));
+  __ blr(AllocateClosureABI::kScratchReg);
 
   // Populate closure object.
-  __ Pop(R1);  // Pop context.
-  __ StoreIntoObject(R0, FieldAddress(R0, target::Closure::context_offset()),
-                     R1);
-  __ PopPair(R3, R1);  // Pop type arguments & extracted method.
+  __ Pop(AllocateClosureABI::kScratchReg);  // Pop context.
+  __ StoreIntoObject(AllocateClosureABI::kResultReg,
+                     FieldAddress(AllocateClosureABI::kResultReg,
+                                  target::Closure::context_offset()),
+                     AllocateClosureABI::kScratchReg);
+  __ Pop(AllocateClosureABI::kScratchReg);  // Pop type arguments.
   __ StoreIntoObjectNoBarrier(
-      R0, FieldAddress(R0, target::Closure::function_offset()), R1);
+      AllocateClosureABI::kResultReg,
+      FieldAddress(AllocateClosureABI::kResultReg,
+                   target::Closure::instantiator_type_arguments_offset()),
+      AllocateClosureABI::kScratchReg);
+  __ LoadObject(AllocateClosureABI::kScratchReg, EmptyTypeArguments());
   __ StoreIntoObjectNoBarrier(
-      R0,
-      FieldAddress(R0, target::Closure::instantiator_type_arguments_offset()),
-      R3);
-  __ LoadObject(R1, EmptyTypeArguments());
-  __ StoreIntoObjectNoBarrier(
-      R0, FieldAddress(R0, target::Closure::delayed_type_arguments_offset()),
-      R1);
+      AllocateClosureABI::kResultReg,
+      FieldAddress(AllocateClosureABI::kResultReg,
+                   target::Closure::delayed_type_arguments_offset()),
+      AllocateClosureABI::kScratchReg);
 
   __ LeaveStubFrame();
+  // No-op if the two are the same.
+  __ MoveRegister(R0, AllocateClosureABI::kResultReg);
   __ Ret();
 }
 
diff --git a/runtime/vm/compiler/stub_code_compiler_x64.cc b/runtime/vm/compiler/stub_code_compiler_x64.cc
index 2dc6cd6..9aa8e8c 100644
--- a/runtime/vm/compiler/stub_code_compiler_x64.cc
+++ b/runtime/vm/compiler/stub_code_compiler_x64.cc
@@ -448,32 +448,39 @@
   __ StoreIntoObject(
       RAX, FieldAddress(RAX, target::Context::variable_offset(0)), RSI);
 
+  // Pop function before pushing context.
+  __ popq(AllocateClosureABI::kFunctionReg);
+
   // Push context.
   __ pushq(RAX);
 
-  // Allocate closure.
+  // Allocate closure. After this point, we only use the registers in
+  // AllocateClosureABI.
   __ LoadObject(CODE_REG, closure_allocation_stub);
-  __ call(FieldAddress(
-      CODE_REG, target::Code::entry_point_offset(CodeEntryKind::kUnchecked)));
+  __ call(FieldAddress(CODE_REG, target::Code::entry_point_offset()));
 
   // Populate closure object.
-  __ popq(RCX);  // Pop context.
-  __ StoreIntoObject(RAX, FieldAddress(RAX, target::Closure::context_offset()),
-                     RCX);
-  __ popq(RCX);  // Pop extracted method.
+  __ popq(AllocateClosureABI::kScratchReg);  // Pop context.
+  __ StoreIntoObject(AllocateClosureABI::kResultReg,
+                     FieldAddress(AllocateClosureABI::kResultReg,
+                                  target::Closure::context_offset()),
+                     AllocateClosureABI::kScratchReg);
+  __ popq(AllocateClosureABI::kScratchReg);  // Pop type argument vector.
   __ StoreIntoObjectNoBarrier(
-      RAX, FieldAddress(RAX, target::Closure::function_offset()), RCX);
-  __ popq(RCX);  // Pop type argument vector.
+      AllocateClosureABI::kResultReg,
+      FieldAddress(AllocateClosureABI::kResultReg,
+                   target::Closure::instantiator_type_arguments_offset()),
+      AllocateClosureABI::kScratchReg);
+  __ LoadObject(AllocateClosureABI::kScratchReg, EmptyTypeArguments());
   __ StoreIntoObjectNoBarrier(
-      RAX,
-      FieldAddress(RAX, target::Closure::instantiator_type_arguments_offset()),
-      RCX);
-  __ LoadObject(RCX, EmptyTypeArguments());
-  __ StoreIntoObjectNoBarrier(
-      RAX, FieldAddress(RAX, target::Closure::delayed_type_arguments_offset()),
-      RCX);
+      AllocateClosureABI::kResultReg,
+      FieldAddress(AllocateClosureABI::kResultReg,
+                   target::Closure::delayed_type_arguments_offset()),
+      AllocateClosureABI::kScratchReg);
 
   __ LeaveStubFrame();
+  // No-op if the two are the same.
+  __ MoveRegister(RAX, AllocateClosureABI::kResultReg);
   __ Ret();
 }
 
diff --git a/runtime/vm/constants_arm.h b/runtime/vm/constants_arm.h
index b0a6159..f638186 100644
--- a/runtime/vm/constants_arm.h
+++ b/runtime/vm/constants_arm.h
@@ -454,6 +454,7 @@
 // ABI for AllocateClosureStub.
 struct AllocateClosureABI {
   static const Register kResultReg = R0;
+  static const Register kFunctionReg = R1;
   static const Register kScratchReg = R4;
 };
 
diff --git a/runtime/vm/constants_arm64.h b/runtime/vm/constants_arm64.h
index ec77884..d82871e 100644
--- a/runtime/vm/constants_arm64.h
+++ b/runtime/vm/constants_arm64.h
@@ -295,6 +295,7 @@
 // ABI for AllocateClosureStub.
 struct AllocateClosureABI {
   static const Register kResultReg = R0;
+  static const Register kFunctionReg = R1;
   static const Register kScratchReg = R4;
 };
 
diff --git a/runtime/vm/constants_ia32.h b/runtime/vm/constants_ia32.h
index 5272b47..3d36faf 100644
--- a/runtime/vm/constants_ia32.h
+++ b/runtime/vm/constants_ia32.h
@@ -205,6 +205,7 @@
 // ABI for AllocateClosureStub.
 struct AllocateClosureABI {
   static const Register kResultReg = EAX;
+  static const Register kFunctionReg = EBX;
   static const Register kScratchReg = EDX;
 };
 
diff --git a/runtime/vm/constants_x64.h b/runtime/vm/constants_x64.h
index 67ef651..469b90b 100644
--- a/runtime/vm/constants_x64.h
+++ b/runtime/vm/constants_x64.h
@@ -266,6 +266,7 @@
 // ABI for AllocateClosureStub.
 struct AllocateClosureABI {
   static const Register kResultReg = RAX;
+  static const Register kFunctionReg = RBX;
   static const Register kScratchReg = R13;
 };
 
diff --git a/runtime/vm/runtime_entry.cc b/runtime/vm/runtime_entry.cc
index 1565559..2b33298 100644
--- a/runtime/vm/runtime_entry.cc
+++ b/runtime/vm/runtime_entry.cc
@@ -595,13 +595,15 @@
   UNREACHABLE();
 }
 
-// Allocate a new closure and initialize its fields to null.
+// Allocate a new closure and initializes its function field with the argument
+// and all other fields to null.
 // Return value: newly allocated closure.
-DEFINE_RUNTIME_ENTRY(AllocateClosure, 0) {
+DEFINE_RUNTIME_ENTRY(AllocateClosure, 1) {
+  const auto& function = Function::CheckedHandle(zone, arguments.ArgAt(0));
   const Closure& closure = Closure::Handle(
       zone, Closure::New(
                 Object::null_type_arguments(), Object::null_type_arguments(),
-                Object::null_type_arguments(), Object::null_function(),
+                Object::null_type_arguments(), function,
                 Context::Handle(Context::null()), SpaceForRuntimeAllocation()));
   arguments.SetReturn(closure);
 }
diff --git a/runtime/vm/service/service.md b/runtime/vm/service/service.md
index 99bdab7..3000c42 100644
--- a/runtime/vm/service/service.md
+++ b/runtime/vm/service/service.md
@@ -1832,7 +1832,7 @@
   int length;
 
   // The enclosing context for this context.
-  Context parent [optional];
+  @Context parent [optional];
 
   // The variables in this context object.
   ContextElement[] variables;
diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn
index 15a1abf..1390092 100644
--- a/sdk/BUILD.gn
+++ b/sdk/BUILD.gn
@@ -252,6 +252,18 @@
   },
 ]
 
+# This rule copies the pre-built DevTools application to
+# bin/resources/devtools/
+copy_tree_specs += [
+  {
+    target = "copy_prebuilt_devtools"
+    visibility = [ ":create_common_sdk" ]
+    source = "../third_party/devtools/web"
+    dest = "$root_out_dir/dart-sdk/bin/resources/devtools"
+    ignore_patterns = "{}"
+  },
+]
+
 # This loop generates rules to copy libraries to lib/
 foreach(library, _full_sdk_libraries) {
   copy_tree_specs += [
@@ -811,6 +823,7 @@
     ":copy_libraries_dart",
     ":copy_libraries_specification",
     ":copy_license",
+    ":copy_prebuilt_devtools",
     ":copy_readme",
     ":copy_vm_dill_files",
     ":write_dartdoc_options",
diff --git a/sdk/lib/_internal/vm/bin/vmservice_io.dart b/sdk/lib/_internal/vm/bin/vmservice_io.dart
index d1f8a5d..833e5a2 100644
--- a/sdk/lib/_internal/vm/bin/vmservice_io.dart
+++ b/sdk/lib/_internal/vm/bin/vmservice_io.dart
@@ -43,6 +43,7 @@
 // HTTP server.
 Server? server;
 Future<Server>? serverFuture;
+_DebuggingSession? ddsInstance;
 
 Server _lazyServerBoot() {
   var localServer = server;
@@ -58,6 +59,90 @@
   return localServer;
 }
 
+/// Responsible for launching a DevTools instance when the service is started
+/// via SIGQUIT.
+class _DebuggingSession {
+  Future<bool> start(
+    String host,
+    String port,
+    bool disableServiceAuthCodes,
+    bool enableDevTools,
+  ) async {
+    final dartPath = Uri.parse(Platform.resolvedExecutable);
+    final dartDir = [
+      '', // Include leading '/'
+      ...dartPath.pathSegments.sublist(
+        0,
+        dartPath.pathSegments.length - 1,
+      ),
+    ].join('/');
+
+    final fullSdk = dartDir.endsWith('bin');
+
+    final ddsSnapshot = [
+      dartDir,
+      fullSdk ? 'snapshots' : 'gen',
+      'dds.dart.snapshot',
+    ].join('/');
+
+    final devToolsBinaries = [
+      dartDir,
+      if (fullSdk) 'resources',
+      'devtools',
+    ].join('/');
+
+    const enableLogging = false;
+    _process = await Process.start(
+      dartPath.toString(),
+      [
+        ddsSnapshot,
+        server!.serverAddress!.toString(),
+        host,
+        port,
+        disableServiceAuthCodes.toString(),
+        enableDevTools.toString(),
+        devToolsBinaries,
+        enableLogging.toString(),
+      ],
+      mode: ProcessStartMode.detachedWithStdio,
+    );
+    final completer = Completer<void>();
+    late StreamSubscription stderrSub;
+    stderrSub = _process!.stderr.transform(utf8.decoder).listen((event) {
+      final result = json.decode(event) as Map<String, dynamic>;
+      final state = result['state'];
+      if (state == 'started') {
+        if (result.containsKey('devToolsUri')) {
+          // NOTE: update pkg/dartdev/lib/src/commands/run.dart if this message
+          // is changed to ensure consistency.
+          const devToolsMessagePrefix =
+              'The Dart DevTools debugger and profiler is available at:';
+          final devToolsUri = result['devToolsUri'];
+          print('$devToolsMessagePrefix $devToolsUri');
+        }
+        stderrSub.cancel();
+        completer.complete();
+      } else {
+        stderrSub.cancel();
+        completer.completeError(
+          'Could not start Observatory HTTP server',
+        );
+      }
+    });
+    try {
+      await completer.future;
+      return true;
+    } catch (e) {
+      stderr.write(e);
+      return false;
+    }
+  }
+
+  void shutdown() => _process!.kill();
+
+  Process? _process;
+}
+
 Future cleanupCallback() async {
   // Cancel the sigquit subscription.
   if (_signalSubscription != null) {
@@ -221,10 +306,6 @@
   _server.acceptNewWebSocketConnections = enable;
 }
 
-void _clearFuture(_) {
-  serverFuture = null;
-}
-
 _onSignal(ProcessSignal signal) {
   if (serverFuture != null) {
     // Still waiting.
@@ -233,9 +314,21 @@
   final _server = _lazyServerBoot();
   // Toggle HTTP server.
   if (_server.running) {
-    _server.shutdown(true).then(_clearFuture);
+    _server.shutdown(true).then((_) async {
+      ddsInstance?.shutdown();
+      await VMService().clearState();
+      serverFuture = null;
+    });
   } else {
-    _server.startup().then(_clearFuture);
+    _server.startup().then((_) {
+      ddsInstance = _DebuggingSession()
+        ..start(
+          _server._ip,
+          _server._port.toString(),
+          false,
+          true,
+        );
+    });
   }
 }
 
diff --git a/sdk/lib/_internal/vm/bin/vmservice_server.dart b/sdk/lib/_internal/vm/bin/vmservice_server.dart
index 69aa7f8..f5742ed 100644
--- a/sdk/lib/_internal/vm/bin/vmservice_server.dart
+++ b/sdk/lib/_internal/vm/bin/vmservice_server.dart
@@ -26,9 +26,9 @@
     socket.done.then((_) => close());
   }
 
-  disconnect() {
+  Future<void> disconnect() async {
     if (socket != null) {
-      socket.close();
+      await socket.close();
     }
   }
 
@@ -102,8 +102,8 @@
   HttpRequestClient(this.request, VMService service)
       : super(service, sendEvents: false);
 
-  disconnect() {
-    request.response.close();
+  Future<void> disconnect() async {
+    await request.response.close();
     close();
   }
 
diff --git a/sdk/lib/vmservice/vmservice.dart b/sdk/lib/vmservice/vmservice.dart
index a3f7e8b..d492dcf 100644
--- a/sdk/lib/vmservice/vmservice.dart
+++ b/sdk/lib/vmservice/vmservice.dart
@@ -411,6 +411,16 @@
     replyPort.send(bytes);
   }
 
+  Future<void> clearState() async {
+    // Create a copy of the set as a list because client.disconnect() will
+    // alter the connected clients set.
+    final clientsList = clients.toList();
+    for (final client in clientsList) {
+      await client.disconnect();
+    }
+    devfs.cleanup();
+  }
+
   Future _exit() async {
     isExiting = true;
 
@@ -423,14 +433,7 @@
     // Close receive ports.
     isolateControlPort.close();
     scriptLoadPort.close();
-
-    // Create a copy of the set as a list because client.disconnect() will
-    // alter the connected clients set.
-    final clientsList = clients.toList();
-    for (final client in clientsList) {
-      client.disconnect();
-    }
-    devfs.cleanup();
+    await clearState();
     final cleanup = VMServiceEmbedderHooks.cleanup;
     if (cleanup != null) {
       await cleanup();
diff --git a/third_party/devtools/update.sh b/third_party/devtools/update.sh
index a69f6ee..077d758 100755
--- a/third_party/devtools/update.sh
+++ b/third_party/devtools/update.sh
@@ -30,12 +30,11 @@
 # to serve from DDS.
 mkdir cipd_package
 cp -R packages/devtools/build/ cipd_package/web
-cp -r packages/devtools_server cipd_package
 cp -r packages/devtools_shared cipd_package
 
 cipd create \
   -name dart/third_party/flutter/devtools \
   -in cipd_package \
   -install-mode copy \
-  -tag revision:$1
+  -tag git_revision:$1
 
diff --git a/tools/VERSION b/tools/VERSION
index ec96746..4dbaebf 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 106
+PRERELEASE 107
 PRERELEASE_PATCH 0
\ No newline at end of file
diff --git a/tools/bots/test_matrix.json b/tools/bots/test_matrix.json
index bf63fe3..39d7272 100644
--- a/tools/bots/test_matrix.json
+++ b/tools/bots/test_matrix.json
@@ -322,6 +322,7 @@
       "xcodebuild/ReleaseSIMARM64C/",
       "xcodebuild/ReleaseX64/",
       "xcodebuild/ReleaseX64C/",
+      "pkg/",
       "samples/",
       "samples_2/",
       "samples-dev/",
@@ -329,6 +330,7 @@
       "third_party/android_tools/sdk/platform-tools/adb",
       "third_party/android_tools/ndk/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip",
       "third_party/android_tools/ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-strip",
+      "third_party/devtools/",
       "third_party/webdriver/",
       "third_party/pkg/",
       "third_party/pkg_tested/",
diff --git a/tools/generate_package_config.dart b/tools/generate_package_config.dart
index 8d4ed5b..f8791cd 100644
--- a/tools/generate_package_config.dart
+++ b/tools/generate_package_config.dart
@@ -57,6 +57,8 @@
     packageDirectory(
         'runtime/observatory_2/tests/service_2/observatory_test_package_2'),
     packageDirectory('sdk/lib/_internal/sdk_library_metadata'),
+    packageDirectory('third_party/devtools/devtools_server'),
+    packageDirectory('third_party/devtools/devtools_shared'),
     packageDirectory('third_party/pkg/protobuf/protobuf'),
     packageDirectory('tools/package_deps'),
   ];
diff --git a/utils/dartdev/BUILD.gn b/utils/dartdev/BUILD.gn
index 7f33d99..3ce3bc3 100644
--- a/utils/dartdev/BUILD.gn
+++ b/utils/dartdev/BUILD.gn
@@ -2,12 +2,14 @@
 # for details. All rights reserved. Use of this source code is governed by a
 # BSD-style license that can be found in the LICENSE file.
 
+import("../../build/dart/copy_tree.gni")
 import("../application_snapshot.gni")
 
 group("dartdev") {
   public_deps = [
     ":copy_dartdev_kernel",
     ":copy_dartdev_snapshot",
+    ":copy_prebuilt_devtools",
   ]
 }
 
@@ -39,3 +41,15 @@
   deps = [ "../dds:dds" ]
   output = "$root_gen_dir/dartdev.dart.snapshot"
 }
+
+copy_trees("copy_prebuilt_devtools") {
+  sources = [
+    {
+      target = "copy_prebuilt_devtools"
+      visibility = [ ":dartdev" ]
+      source = "../../third_party/devtools/web"
+      dest = "$root_out_dir/devtools"
+      ignore_patterns = "{}"
+    },
+  ]
+}