Version 2.14.0-319.0.dev

Merge commit 'a99b92c2525c6e490e2a55fce2888d251688b67a' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index 204df98..3174dac 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -11,7 +11,7 @@
     "constraint, update this by running tools/generate_package_config.dart."
   ],
   "configVersion": 2,
-  "generated": "2021-07-02T15:48:57.033703",
+  "generated": "2021-07-14T10:43:41.119864",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
diff --git a/DEPS b/DEPS
index 44d5d82..bbcf4ae 100644
--- a/DEPS
+++ b/DEPS
@@ -164,7 +164,7 @@
   "test_process_tag": "2.0.0",
   "term_glyph_rev": "6a0f9b6fb645ba75e7a00a4e20072678327a0347",
   "test_reflective_loader_rev": "54e930a11c372683792e22bddad79197728c91ce",
-  "test_rev": "030816c32b6fe78d5fb7653afbd9f63cca18bacf",
+  "test_rev": "099dcc4d052a30c6921489cfbefa1c8531d12975",
   "typed_data_tag": "f94fc57b8e8c0e4fe4ff6cfd8290b94af52d3719",
   "usage_rev": "e0780cd8b2f8af69a28dc52678ffe8492da27d06",
   "vector_math_rev": "0c9f5d68c047813a6dcdeb88ba7a42daddf25025",
diff --git a/pkg/analyzer/lib/src/context/builder.dart b/pkg/analyzer/lib/src/context/builder.dart
index 502ea26..ec0aaa7 100644
--- a/pkg/analyzer/lib/src/context/builder.dart
+++ b/pkg/analyzer/lib/src/context/builder.dart
@@ -8,25 +8,6 @@
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:yaml/yaml.dart';
 
-/// TODO(scheglov) It is not used, inline it.
-class ContextBuilderOptions {
-  /// The file path of the analysis options file that should be used in place of
-  /// any file in the root directory or a parent of the root directory, or `null`
-  /// if the normal lookup mechanism should be used.
-  String? defaultAnalysisOptionsFilePath;
-
-  /// A table mapping variable names to values for the declared variables.
-  Map<String, String> declaredVariables = {};
-
-  /// The file path of the .packages file that should be used in place of any
-  /// file found using the normal (Package Specification DEP) lookup mechanism,
-  /// or `null` if the normal lookup mechanism should be used.
-  String? defaultPackageFilePath;
-
-  /// Initialize a newly created set of options
-  ContextBuilderOptions();
-}
-
 /// Given a package map, check in each package's lib directory for the existence
 /// of an `_embedder.yaml` file. If the file contains a top level YamlMap, it
 /// will be added to the [embedderYamls] map.
diff --git a/pkg/analyzer_cli/lib/src/driver.dart b/pkg/analyzer_cli/lib/src/driver.dart
index 5b0948e..a3b5448 100644
--- a/pkg/analyzer_cli/lib/src/driver.dart
+++ b/pkg/analyzer_cli/lib/src/driver.dart
@@ -431,7 +431,7 @@
   }
 
   void _verifyAnalysisOptionsFileExists(CommandLineOptions options) {
-    var path = options.analysisOptionsFile;
+    var path = options.defaultAnalysisOptionsPath;
     if (path != null) {
       if (!resourceProvider.getFile(path).exists) {
         printAndFail('Options file not found: $path',
@@ -449,8 +449,8 @@
       CommandLineOptions previous, CommandLineOptions newOptions) {
     return previous != null &&
         newOptions != null &&
-        newOptions.packageConfigPath == previous.packageConfigPath &&
-        _equalMaps(newOptions.definedVariables, previous.definedVariables) &&
+        newOptions.defaultPackagesPath == previous.defaultPackagesPath &&
+        _equalMaps(newOptions.declaredVariables, previous.declaredVariables) &&
         newOptions.log == previous.log &&
         newOptions.disableHints == previous.disableHints &&
         newOptions.showPackageWarnings == previous.showPackageWarnings &&
@@ -554,8 +554,8 @@
     _collection = AnalysisContextCollectionImpl(
       byteStore: Driver.analysisDriverMemoryByteStore,
       includedPaths: _pathList,
-      optionsFile: _commandLineOptions.analysisOptionsFile,
-      packagesFile: _commandLineOptions.packageConfigPath,
+      optionsFile: _commandLineOptions.defaultAnalysisOptionsPath,
+      packagesFile: _commandLineOptions.defaultPackagesPath,
       resourceProvider: _resourceProvider,
       sdkPath: _commandLineOptions.dartSdkPath,
       updateAnalysisOptions: _updateAnalysisOptions,
diff --git a/pkg/analyzer_cli/lib/src/options.dart b/pkg/analyzer_cli/lib/src/options.dart
index bb2da44..7ec27d6 100644
--- a/pkg/analyzer_cli/lib/src/options.dart
+++ b/pkg/analyzer_cli/lib/src/options.dart
@@ -6,7 +6,6 @@
 
 import 'package:analyzer/dart/analysis/features.dart';
 import 'package:analyzer/file_system/file_system.dart';
-import 'package:analyzer/src/context/builder.dart';
 import 'package:analyzer/src/dart/analysis/experiments.dart';
 import 'package:analyzer/src/generated/engine.dart' show AnalysisOptionsImpl;
 import 'package:analyzer/src/util/file_paths.dart' as file_paths;
@@ -51,8 +50,18 @@
 class CommandLineOptions {
   final ArgResults _argResults;
 
-  /// The options defining the context in which analysis is performed.
-  final ContextBuilderOptions contextBuilderOptions;
+  /// The file path of the analysis options file that should be used in place of
+  /// any file in the root directory or a parent of the root directory,
+  /// or `null` if the normal lookup mechanism should be used.
+  String defaultAnalysisOptionsPath;
+
+  /// The file path of the .packages file that should be used in place of any
+  /// file found using the normal (Package Specification DEP) lookup mechanism,
+  /// or `null` if the normal lookup mechanism should be used.
+  String defaultPackagesPath;
+
+  /// A table mapping variable names to values for the declared variables.
+  final Map<String, String> declaredVariables = {};
 
   /// The path to the dart SDK.
   String dartSdkPath;
@@ -124,10 +133,6 @@
     ResourceProvider resourceProvider,
     ArgResults args,
   )   : _argResults = args,
-        contextBuilderOptions = _createContextBuilderOptions(
-          resourceProvider,
-          args,
-        ),
         dartSdkPath = cast(args[_sdkPathOption]),
         disableCacheFlushing = cast(args['disable-cache-flushing']),
         disableHints = cast(args['no-hints']),
@@ -149,11 +154,41 @@
         lintsAreFatal = cast(args['fatal-lints']),
         trainSnapshot = cast(args['train-snapshot']),
         verbose = cast(args['verbose']),
-        color = cast(args['color']);
+        color = cast(args['color']) {
+    //
+    // File locations.
+    //
+    defaultAnalysisOptionsPath = _absoluteNormalizedPath(
+      resourceProvider,
+      cast(args[_analysisOptionsFileOption]),
+    );
+    defaultPackagesPath = _absoluteNormalizedPath(
+      resourceProvider,
+      cast(args[_packagesOption]),
+    );
 
-  /// The path to an analysis options file
-  String get analysisOptionsFile =>
-      contextBuilderOptions.defaultAnalysisOptionsFilePath;
+    //
+    // Declared variables.
+    //
+    var variables = (args[_defineVariableOption] as List).cast<String>();
+    for (var variable in variables) {
+      var index = variable.indexOf('=');
+      if (index < 0) {
+        // TODO (brianwilkerson) Decide the semantics we want in this case.
+        // The VM prints "No value given to -D option", then tries to load '-Dfoo'
+        // as a file and dies. Unless there was nothing after the '-D', in which
+        // case it prints the warning and ignores the option.
+      } else {
+        var name = variable.substring(0, index);
+        if (name.isNotEmpty) {
+          // TODO (brianwilkerson) Decide the semantics we want in the case where
+          // there is no name. If there is no name, the VM tries to load a file
+          // named '-D' and dies.
+          declaredVariables[name] = variable.substring(index + 1);
+        }
+      }
+    }
+  }
 
   /// The default language version for files that are not in a package.
   /// (Or null if no default language version to force.)
@@ -161,10 +196,6 @@
     return cast(_argResults[_defaultLanguageVersionOption]);
   }
 
-  /// A table mapping the names of defined variables to their values.
-  Map<String, String> get definedVariables =>
-      contextBuilderOptions.declaredVariables;
-
   /// A list of the names of the experiments that are to be enabled.
   List<String> get enabledExperiments {
     return cast(_argResults[_enableExperimentOption]);
@@ -176,9 +207,6 @@
 
   bool get noImplicitDynamic => _argResults[_noImplicitDynamicFlag] as bool;
 
-  /// The path to a `.packages` configuration file
-  String get packageConfigPath => contextBuilderOptions.defaultPackageFilePath;
-
   /// Update the [analysisOptions] with flags that the user specified
   /// explicitly. The [analysisOptions] are usually loaded from one of
   /// `analysis_options.yaml` files, possibly with includes. We consider
@@ -307,57 +335,17 @@
     return options;
   }
 
-  /// Use the command-line [args] to create a context builder options.
-  static ContextBuilderOptions _createContextBuilderOptions(
+  static String _absoluteNormalizedPath(
     ResourceProvider resourceProvider,
-    ArgResults args,
+    String path,
   ) {
-    String absoluteNormalizedPath(String path) {
-      if (path == null) {
-        return null;
-      }
-      var pathContext = resourceProvider.pathContext;
-      return pathContext.normalize(
-        pathContext.absolute(path),
-      );
+    if (path == null) {
+      return null;
     }
-
-    var builderOptions = ContextBuilderOptions();
-
-    //
-    // File locations.
-    //
-    builderOptions.defaultAnalysisOptionsFilePath = absoluteNormalizedPath(
-      cast(args[_analysisOptionsFileOption]),
+    var pathContext = resourceProvider.pathContext;
+    return pathContext.normalize(
+      pathContext.absolute(path),
     );
-    builderOptions.defaultPackageFilePath = absoluteNormalizedPath(
-      cast(args[_packagesOption]),
-    );
-    //
-    // Declared variables.
-    //
-    var declaredVariables = <String, String>{};
-    var variables = (args[_defineVariableOption] as List).cast<String>();
-    for (var variable in variables) {
-      var index = variable.indexOf('=');
-      if (index < 0) {
-        // TODO (brianwilkerson) Decide the semantics we want in this case.
-        // The VM prints "No value given to -D option", then tries to load '-Dfoo'
-        // as a file and dies. Unless there was nothing after the '-D', in which
-        // case it prints the warning and ignores the option.
-      } else {
-        var name = variable.substring(0, index);
-        if (name.isNotEmpty) {
-          // TODO (brianwilkerson) Decide the semantics we want in the case where
-          // there is no name. If there is no name, the VM tries to load a file
-          // named '-D' and dies.
-          declaredVariables[name] = variable.substring(index + 1);
-        }
-      }
-    }
-    builderOptions.declaredVariables = declaredVariables;
-
-    return builderOptions;
   }
 
   /// Add the standard flags and options to the given [parser]. The standard flags
diff --git a/pkg/analyzer_cli/lib/src/perf_report.dart b/pkg/analyzer_cli/lib/src/perf_report.dart
index 25c21c5..04caa35 100644
--- a/pkg/analyzer_cli/lib/src/perf_report.dart
+++ b/pkg/analyzer_cli/lib/src/perf_report.dart
@@ -38,8 +38,8 @@
     'showPackageWarnings': options.showPackageWarnings,
     'showPackageWarningsPrefix': options.showPackageWarningsPrefix,
     'showSdkWarnings': options.showSdkWarnings,
-    'definedVariables': options.definedVariables,
-    'packageConfigPath': options.packageConfigPath,
+    'definedVariables': options.declaredVariables,
+    'packageConfigPath': options.defaultPackagesPath,
     'sourceFiles': options.sourceFiles,
   };
 
diff --git a/pkg/analyzer_cli/test/options_test.dart b/pkg/analyzer_cli/test/options_test.dart
index 8d3a0fa..4b7167b 100644
--- a/pkg/analyzer_cli/test/options_test.dart
+++ b/pkg/analyzer_cli/test/options_test.dart
@@ -84,8 +84,8 @@
 
       test('defined variables', () {
         var options = parse(['--dart-sdk', '.', '-Dfoo=bar', 'foo.dart']);
-        expect(options.definedVariables['foo'], equals('bar'));
-        expect(options.definedVariables['bar'], isNull);
+        expect(options.declaredVariables['foo'], equals('bar'));
+        expect(options.declaredVariables['bar'], isNull);
       });
 
       test('disable cache flushing', () {
@@ -210,7 +210,7 @@
       test('options', () {
         var options =
             parse(['--dart-sdk', '.', '--options', 'options.yaml', 'foo.dart']);
-        expect(options.analysisOptionsFile, endsWith('options.yaml'));
+        expect(options.defaultAnalysisOptionsPath, endsWith('options.yaml'));
       });
 
       test('lints', () {
@@ -284,8 +284,7 @@
   void test_declaredVariables() {
     _parse(['-Da=0', '-Db=', 'a.dart']);
 
-    var options = commandLineOptions.contextBuilderOptions;
-    var definedVariables = options.declaredVariables;
+    var definedVariables = commandLineOptions.declaredVariables;
 
     expect(definedVariables['a'], '0');
     expect(definedVariables['b'], '');
@@ -296,9 +295,8 @@
     var expected = 'my_options.yaml';
     _parse(['--options=$expected', 'a.dart']);
 
-    var builderOptions = commandLineOptions.contextBuilderOptions;
     expect(
-      builderOptions.defaultAnalysisOptionsFilePath,
+      commandLineOptions.defaultAnalysisOptionsPath,
       endsWith(expected),
     );
   }
@@ -307,20 +305,17 @@
     var expected = 'my_package_config.json';
     _parse(['--packages=$expected', 'a.dart']);
 
-    var builderOptions = commandLineOptions.contextBuilderOptions;
     expect(
-      builderOptions.defaultPackageFilePath,
+      commandLineOptions.defaultPackagesPath,
       endsWith(expected),
     );
   }
 
   void test_defaults() {
     _parse(['a.dart']);
-    var builderOptions = commandLineOptions.contextBuilderOptions;
-    expect(builderOptions, isNotNull);
-    expect(builderOptions.declaredVariables, isEmpty);
-    expect(builderOptions.defaultAnalysisOptionsFilePath, isNull);
-    expect(builderOptions.defaultPackageFilePath, isNull);
+    expect(commandLineOptions.declaredVariables, isEmpty);
+    expect(commandLineOptions.defaultAnalysisOptionsPath, isNull);
+    expect(commandLineOptions.defaultPackagesPath, isNull);
   }
 
   void test_filterUnknownArguments() {
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 84e2e6a..9859145 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 2.0.2
+- Fix possibility of `LateInitializationError` being thrown when trying to
+  cleanup after an error during initialization.
+
 # 2.0.1
 - Update `package:vm_service` to ^7.0.0.
 
diff --git a/pkg/dds/lib/src/dds_impl.dart b/pkg/dds/lib/src/dds_impl.dart
index c65ba5d..4360bcc 100644
--- a/pkg/dds/lib/src/dds_impl.dart
+++ b/pkg/dds/lib/src/dds_impl.dart
@@ -67,7 +67,7 @@
 
   Future<void> startService() async {
     bool started = false;
-    final completer = Completer<void>();
+    DartDevelopmentServiceException? error;
     // TODO(bkonyi): throw if we've already shutdown.
     // Establish the connection to the VM service.
     _vmServiceSocket = webSocketBuilder(remoteVmServiceWsUri);
@@ -76,18 +76,25 @@
     unawaited(
       vmServiceClient.listen().then(
         (_) {
-          shutdown();
-          if (!started && !completer.isCompleted) {
-            completer
-                .completeError(DartDevelopmentServiceException.failedToStart());
+          if (started) {
+            shutdown();
+          } else {
+            // If we fail to connect to the service or the connection is
+            // terminated while we're starting up, we'll need to cleanup later
+            // once DDS has finished initializing to make sure all ports are
+            // closed before throwing the exception.
+            error = DartDevelopmentServiceException.failedToStart();
           }
         },
         onError: (e, st) {
-          shutdown();
-          if (!completer.isCompleted) {
-            completer.completeError(
-              DartDevelopmentServiceException.connectionIssue(e.toString()),
-              st,
+          if (started) {
+            shutdown();
+          } else {
+            // If we encounter an error while we're starting up, we'll need to
+            // cleanup later once DDS has finished initializing to make sure
+            // all ports are closed before throwing the exception.
+            error = DartDevelopmentServiceException.connectionIssue(
+              e.toString(),
             );
           }
         },
@@ -104,13 +111,15 @@
       // Once we have a connection to the VM service, we're ready to spawn the intermediary.
       await _startDDSServer();
       started = true;
-      completer.complete();
     } on StateError {
       /* Ignore json-rpc state errors */
-    } catch (e, st) {
-      completer.completeError(e, st);
     }
-    return completer.future;
+
+    // Check if we encountered any errors during startup, cleanup, and throw.
+    if (error != null) {
+      await shutdown();
+      throw error!;
+    }
   }
 
   Future<void> _startDDSServer() async {
diff --git a/pkg/dds/lib/src/devtools/devtools_client.dart b/pkg/dds/lib/src/devtools/devtools_client.dart
index e5ec54e..8bc3564 100644
--- a/pkg/dds/lib/src/devtools/devtools_client.dart
+++ b/pkg/dds/lib/src/devtools/devtools_client.dart
@@ -4,11 +4,12 @@
 
 import 'dart:async';
 
-import 'package:devtools_shared/devtools_server.dart';
 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);
 
diff --git a/pkg/dds/lib/src/devtools/devtools_handler.dart b/pkg/dds/lib/src/devtools/devtools_handler.dart
index 3b55bfb..87c4fa6 100644
--- a/pkg/dds/lib/src/devtools/devtools_handler.dart
+++ b/pkg/dds/lib/src/devtools/devtools_handler.dart
@@ -5,13 +5,13 @@
 import 'dart:async';
 
 import 'package:dds/src/constants.dart';
-import 'package:devtools_shared/devtools_server.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/.
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..d5d6c6a
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/file_system.dart
@@ -0,0 +1,82 @@
+// 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.
+
+// 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..5f21a03
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/server_api.dart
@@ -0,0 +1,228 @@
+// 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.
+
+// 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..8e61b90
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/usage.dart
@@ -0,0 +1,227 @@
+// 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.
+
+// 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, '');
+  }
+
+  late 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;
+
+  late 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 == surveyName);
+    rewriteActiveSurvey(false, 0);
+  }
+
+  String? get activeSurvey => _activeSurvey;
+
+  set activeSurvey(String? surveyName) {
+    _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) {
+    properties[activeSurvey] = {
+      _surveyActionTaken: actionTaken,
+      _surveyShownCount: shownCount,
+    };
+  }
+
+  int? get surveyShownCount {
+    final prop = properties[activeSurvey];
+    if (prop[_surveyShownCount] == null) {
+      rewriteActiveSurvey(prop[_surveyActionTaken], 0);
+    }
+    return properties[activeSurvey][_surveyShownCount];
+  }
+
+  void incrementSurveyShownCount() {
+    surveyShownCount; // Ensure surveyShownCount has been initialized.
+    final prop = properties[activeSurvey];
+    rewriteActiveSurvey(prop[_surveyActionTaken], prop[_surveyShownCount] + 1);
+  }
+
+  bool get surveyActionTaken {
+    return properties[activeSurvey][_surveyActionTaken] == true;
+  }
+
+  set surveyActionTaken(bool? value) {
+    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();
+  }
+
+  late 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 62298b1..012f3e3 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: 2.0.1
+version: 2.0.2
 
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/dds
 
@@ -24,6 +24,7 @@
   shelf_web_socket: ^1.0.0
   sse: ^4.0.0
   stream_channel: ^2.0.0
+  usage: ^4.0.0
   vm_service: ^7.0.0
   web_socket_channel: ^2.0.0
 
diff --git a/pkg/dds/test/common/fakes.dart b/pkg/dds/test/common/fakes.dart
index 7e11f9d..9ef5cf9 100644
--- a/pkg/dds/test/common/fakes.dart
+++ b/pkg/dds/test/common/fakes.dart
@@ -7,6 +7,7 @@
 import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
 import 'package:test/fake.dart';
 import 'package:vm_service/vm_service.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
 
 /// [FakePeer] implements the bare minimum of the [Peer] interface needed for
 /// [DartDevelopmentService] to establish a connection with a VM service.
@@ -62,3 +63,18 @@
 
   int _idCount = 0;
 }
+
+class FakeWebSocketSink extends Fake implements WebSocketSink {
+  @override
+  Future close([int? closeCode, String? closeReason]) {
+    // Do nothing.
+    return Future.value();
+  }
+}
+
+/// [FakeWebSocketChannel] implements the bare minimum of the [WebSocket]
+/// interface required to finish DDS initialization.
+class FakeWebSocketChannel extends Fake implements WebSocketChannel {
+  @override
+  WebSocketSink get sink => FakeWebSocketSink();
+}
diff --git a/pkg/dds/test/handles_client_disconnect_state_error_test.dart b/pkg/dds/test/handles_client_disconnect_state_error_test.dart
index 816703b..c9d35fa 100644
--- a/pkg/dds/test/handles_client_disconnect_state_error_test.dart
+++ b/pkg/dds/test/handles_client_disconnect_state_error_test.dart
@@ -9,7 +9,6 @@
 import 'package:dds/src/rpc_error_codes.dart';
 import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
 import 'package:pedantic/pedantic.dart';
-import 'package:test/fake.dart';
 import 'package:test/test.dart';
 import 'package:web_socket_channel/web_socket_channel.dart';
 
@@ -42,19 +41,6 @@
   }
 }
 
-class FakeWebSocketSink extends Fake implements WebSocketSink {
-  @override
-  Future close([int? closeCode, String? closeReason]) {
-    // Do nothing.
-    return Future.value();
-  }
-}
-
-class FakeWebSocketChannel extends Fake implements WebSocketChannel {
-  @override
-  WebSocketSink get sink => FakeWebSocketSink();
-}
-
 void main() {
   webSocketBuilder = (Uri _) => FakeWebSocketChannel();
   peerBuilder =
diff --git a/pkg/dds/test/handles_shutdown_before_startup_test.dart b/pkg/dds/test/handles_shutdown_before_startup_test.dart
new file mode 100644
index 0000000..ae1b06a
--- /dev/null
+++ b/pkg/dds/test/handles_shutdown_before_startup_test.dart
@@ -0,0 +1,54 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:dds/dds.dart';
+import 'package:dds/src/dds_impl.dart';
+import 'package:test/test.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import 'common/fakes.dart';
+
+class ImmediatelyClosedPeer extends FakePeer {
+  // Immediately complete the returned future to act as if we've closed.
+  @override
+  Future<void> listen() => Future.value();
+}
+
+class ImmediatelyClosedErrorPeer extends FakePeer {
+  // Immediately complete the returned future with an error to act as if we've
+  // encountered a connection issue.
+  @override
+  Future<void> listen() => Future.error('Connection lost');
+}
+
+// Regression test for https://github.com/flutter/flutter/issues/86361.
+void main() {
+  webSocketBuilder = (Uri _) => FakeWebSocketChannel();
+
+  test('Shutdown before server startup complete', () async {
+    peerBuilder =
+        (WebSocketChannel _, dynamic __) async => ImmediatelyClosedPeer();
+    try {
+      await DartDevelopmentService.startDartDevelopmentService(
+        Uri(scheme: 'http'),
+      );
+    } on DartDevelopmentServiceException {
+      /* We expect to fail to start */
+    }
+  });
+
+  test('Error shutdown before server startup complete', () async {
+    peerBuilder =
+        (WebSocketChannel _, dynamic __) async => ImmediatelyClosedErrorPeer();
+    try {
+      await DartDevelopmentService.startDartDevelopmentService(
+        Uri(scheme: 'http'),
+      );
+    } on DartDevelopmentServiceException {
+      /* We expect to fail to start */
+    }
+  });
+}
diff --git a/runtime/vm/virtual_memory_win.cc b/runtime/vm/virtual_memory_win.cc
index c4f4269..1bab7a3 100644
--- a/runtime/vm/virtual_memory_win.cc
+++ b/runtime/vm/virtual_memory_win.cc
@@ -62,10 +62,7 @@
                             PAGE_READWRITE, nullptr);
     if (address == nullptr) {
       int error = GetLastError();
-      const int kBufferSize = 1024;
-      char error_buf[kBufferSize];
-      FATAL2("Failed to reserve region for compressed heap: %d (%s)", error,
-             Utils::StrError(error, error_buf, kBufferSize));
+      FATAL2("Failed to reserve region for compressed heap: %d", error);
     }
     VirtualMemoryCompressedHeap::Init(address);
   }
diff --git a/tools/VERSION b/tools/VERSION
index 7d2ebaa..75a9854 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 318
+PRERELEASE 319
 PRERELEASE_PATCH 0
\ No newline at end of file