Add auto-snapshotting and analytics for memory usage.

See video linked to issue: https://github.com/flutter/devtools/issues/5606

Change-Id: I9f22031871e30bc7160e2c49b0423fb64df62223
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/300862
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Jacob Richman <jacobr@google.com>
diff --git a/DEPS b/DEPS
index 853252e..9a5b506 100644
--- a/DEPS
+++ b/DEPS
@@ -173,7 +173,7 @@
   "test_descriptor_rev": "23e49a21fc6b4bf3164d336c699b29d1b8bb4622",
   "test_process_rev": "b6a6cd5f598250c71d8deeea3d38eb821af5e932",
   "test_reflective_loader_rev": "d1b763f6281a46a48e1da6f0a6e8152e3480e8d2",
-  "tools_rev": "62c96040d8090bc1bb866db75d40bd1707876cf9",
+  "tools_rev": "49da4cabaddec3c82485b15d83ddac2278f947db",
   "typed_data_rev": "921f5c0380c6c487d8a065c45618162efa4cbd92",
   "usage_rev": "929a4e31f0bd4f861dd0e34d4c6f7184c751b569",
   "vector_math_rev": "e3de8da3e7db3b9b4f56a16061e9e480539fb08c",
diff --git a/pkg/analysis_server/lib/src/analytics/analytics_manager.dart b/pkg/analysis_server/lib/src/analytics/analytics_manager.dart
index a283aa1..c2eedd8 100644
--- a/pkg/analysis_server/lib/src/analytics/analytics_manager.dart
+++ b/pkg/analysis_server/lib/src/analytics/analytics_manager.dart
@@ -20,6 +20,7 @@
 import 'package:analysis_server/src/status/pages.dart';
 import 'package:analyzer/dart/analysis/analysis_context.dart';
 import 'package:collection/collection.dart';
+import 'package:leak_tracker/src/usage_tracking/model.dart';
 import 'package:unified_analytics/unified_analytics.dart';
 
 /// An interface for managing and reporting analytics.
@@ -190,6 +191,28 @@
     requestData.addValue('openWorkspacePaths', openWorkspacePaths.length);
   }
 
+  Future<void> sendMemoryUsage(MemoryUsageEvent event) async {
+    final delta = event.delta;
+    var seconds = event.period?.inSeconds;
+
+    assert((event.delta == null) == (event.period == null));
+
+    if (delta == null || seconds == null) {
+      await analytics.sendEvent(eventName: DashEvent.memoryInfo, eventData: {
+        'rss': event.rss,
+      });
+      return;
+    }
+
+    if (seconds == 0) seconds = 1;
+
+    await analytics.sendEvent(eventName: DashEvent.memoryInfo, eventData: {
+      'rss': event.rss,
+      'periodSec': seconds,
+      'mbPerSec': delta / seconds,
+    });
+  }
+
   /// Record that the given [response] was sent to the client.
   void sentResponse({required Response response}) {
     var sendTime = DateTime.now();
diff --git a/pkg/analysis_server/lib/src/server/driver.dart b/pkg/analysis_server/lib/src/server/driver.dart
index 05a8dd9..08f4305 100644
--- a/pkg/analysis_server/lib/src/server/driver.dart
+++ b/pkg/analysis_server/lib/src/server/driver.dart
@@ -41,6 +41,8 @@
 import 'package:telemetry/telemetry.dart' as telemetry;
 import 'package:unified_analytics/unified_analytics.dart';
 
+import '../utilities/usage_tracking/usage_tracking.dart';
+
 /// The [Driver] class represents a single running instance of the analysis
 /// server application.  It is responsible for parsing command line options
 /// and starting the HTTP and/or stdio servers.
@@ -355,6 +357,11 @@
           errorNotifier,
           sendPort);
     }
+
+    configureMemoryUsageTracking(
+      arguments,
+      (memoryUsageEvent) => analyticsManager.sendMemoryUsage(memoryUsageEvent),
+    );
   }
 
   void startAnalysisServer(
diff --git a/pkg/analysis_server/lib/src/utilities/usage_tracking/AUTOSNAPSHOTTING.md b/pkg/analysis_server/lib/src/utilities/usage_tracking/AUTOSNAPSHOTTING.md
new file mode 100644
index 0000000..7e2dd75
--- /dev/null
+++ b/pkg/analysis_server/lib/src/utilities/usage_tracking/AUTOSNAPSHOTTING.md
@@ -0,0 +1,50 @@
+# Auto-snapshotting
+
+IMPORTANT: memory snapshots should not be requested from external users because they may contain PII.
+
+If a user reports that the process `dart:analysis_server.dart.snapshot` takes too much memory,
+and the issue is hard to reproduce, you may want to request memory snapshots from the user.
+
+## Request numbers
+
+Ask user to provide memory footprint for the process `dart:analysis_server.dart.snapshot`.
+If there are many instances of the process, ask for the biggest memory footprint among
+the instances.
+
+- **Mac**: column 'Real Mem' in 'Activity Monitor'
+- **Windows**: TODO: add content
+- **Linux**: TODO: add content
+
+## Create auto-snapshotting argument
+
+Based on the reported and expected values, construct auto-snapshotting argument. See example in
+the [test file](../../../../test/utilities/autosnapshotting/autosnapshotting_test.dart), the
+constant `_autosnapshottingArg`.
+
+See explanation of parameters in
+[documentation for AutoSnapshottingConfig](https://github.com/dart-lang/leak_tracker/blob/main/lib/src/autosnapshotting/model.dart).
+
+## Instruct user to configure analyzer
+
+Pass the created argument to the user and instruct them to configure
+analyzer.
+
+### For VSCode
+
+1. Open Settings > Extensions > Dart > Analyser
+2. Add the argument to `Dart: Analyzer Additional Args`
+
+### For Android Studio
+
+1. Double-press Shift
+2. Type 'Registry' into search field
+3. Click 'Registry...'
+4. Add the argument to the value of the key 'dart.server.additional.arguments'
+
+## Analyze snapshots
+
+Ask user to provide the collected snapshots and analyze them.
+
+TODO (polina-c): link DevTools documentation
+
+
diff --git a/pkg/analysis_server/lib/src/utilities/usage_tracking/usage_tracking.dart b/pkg/analysis_server/lib/src/utilities/usage_tracking/usage_tracking.dart
new file mode 100644
index 0000000..2f658a7
--- /dev/null
+++ b/pkg/analysis_server/lib/src/utilities/usage_tracking/usage_tracking.dart
@@ -0,0 +1,97 @@
+// Copyright (c) 2023, 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 'package:args/args.dart';
+import 'package:collection/collection.dart';
+import 'package:leak_tracker/src/usage_tracking/model.dart';
+import 'package:leak_tracker/src/usage_tracking/usage_tracking.dart';
+
+void configureMemoryUsageTracking(
+  List<String> arguments,
+  UsageCallback callback,
+) {
+  final config = UsageTrackingConfig(
+    interval: const Duration(seconds: 1),
+    usageEventsConfig: UsageEventsConfig(
+      callback,
+      deltaMb: 512,
+    ),
+    autoSnapshottingConfig: parseAutoSnapshottingConfig(arguments),
+  );
+
+  trackMemoryUsage(config);
+}
+
+/// Parses the config for autosnapshotting from CLI [args].
+///
+/// See example of config in tests for this function.
+///
+/// If there is no argument that starts with '--autosnapshotting=', returns null.
+///
+/// In case of error throws exception.
+AutoSnapshottingConfig? parseAutoSnapshottingConfig(List<String> args) {
+  const argName = 'autosnapshotting';
+  final arg = args.firstWhereOrNull((a) => a.contains('--$argName'));
+  if (arg == null) return null;
+
+  var parser = ArgParser()..addMultiOption(argName, splitCommas: true);
+
+  final parsedArgs = parser.parse([arg]);
+  assert(parsedArgs.options.contains(argName));
+
+  final values = parsedArgs[argName] as List<String>;
+  if (values.isEmpty) return null;
+
+  final items = Map.fromEntries(values.map((e) {
+    final keyValue = e.split('=');
+    if (keyValue.length != 2) {
+      throw ArgumentError(
+        'Invalid auto-snapshotting config: $values.\n'
+        'Expected "key-value", got "$e".',
+      );
+    }
+
+    final keyString = keyValue[0];
+    try {
+      final key = _Keys.values.byName(keyString);
+      return MapEntry(key, keyValue[1]);
+    } on ArgumentError {
+      throw ArgumentError('Invalid auto-snapshotting key: $keyString".');
+    }
+  }));
+
+  if (!items.containsKey(_Keys.dir)) {
+    throw ArgumentError(
+        '${_Keys.dir.name} should be provided for auto-snapshotting.');
+  }
+
+  return AutoSnapshottingConfig(
+    thresholdMb: _parseKey(_Keys.thresholdMb, items, 7000),
+    increaseMb: _parseKey(_Keys.increaseMb, items, 500),
+    directory: items[_Keys.dir]!,
+    directorySizeLimitMb: _parseKey(_Keys.dirLimitMb, items, 30000),
+    minDelayBetweenSnapshots: Duration(
+      seconds: _parseKey(_Keys.delaySec, items, 20),
+    ),
+  );
+}
+
+int _parseKey(_Keys key, Map<_Keys, String> items, int defaultValue) {
+  final value = items[key];
+  if (value == null || value.trim().isEmpty) return defaultValue;
+  final result = int.tryParse(value);
+  if (result == null) {
+    throw ArgumentError(
+        'Invalid auto-snapshotting value for ${key.name}: $value.');
+  }
+  return result;
+}
+
+enum _Keys {
+  thresholdMb,
+  increaseMb,
+  dir,
+  dirLimitMb,
+  delaySec,
+}
diff --git a/pkg/analysis_server/pubspec.yaml b/pkg/analysis_server/pubspec.yaml
index c7983da..fa70ec8 100644
--- a/pkg/analysis_server/pubspec.yaml
+++ b/pkg/analysis_server/pubspec.yaml
@@ -16,6 +16,7 @@
   crypto: any
   dart_style: any
   http: any
+  leak_tracker: any
   linter: any
   meta: any
   path: any
diff --git a/pkg/analysis_server/test/utilities/usage_tracking/usage_tracking_test.dart b/pkg/analysis_server/test/utilities/usage_tracking/usage_tracking_test.dart
new file mode 100644
index 0000000..0c396b6
--- /dev/null
+++ b/pkg/analysis_server/test/utilities/usage_tracking/usage_tracking_test.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2023, 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 'package:analysis_server/src/utilities/usage_tracking/usage_tracking.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('parseAutoSnapshottingConfig', () {
+    test('parses correct config', () {
+      final config = parseAutoSnapshottingConfig(_argsWithSnapshotting)!;
+
+      expect(config.thresholdMb, 200);
+      expect(config.increaseMb, 100);
+      expect(config.directory, '/Users/polinach/Downloads/analyzer_snapshots');
+      expect(config.directorySizeLimitMb, 10000);
+      expect(config.minDelayBetweenSnapshots, Duration(seconds: 20));
+    });
+
+    test('returns null for no config', () {
+      final config = parseAutoSnapshottingConfig(_argsNoSnapshotting);
+      expect(config, null);
+    });
+
+    test('throws for wrong config', () {
+      final wrongAutosnapshottingArg =
+          '--autosnapshotting--wrong-configuration';
+
+      expect(
+        () => parseAutoSnapshottingConfig(
+            [wrongAutosnapshottingArg, 'some other arg']),
+        throwsA(isA<Object>()),
+      );
+    });
+  });
+}
+
+const _argsNoSnapshotting = [
+  '--sdk=C:/b/s/w/ir/x/w/sdk/sdk/',
+  '--train-using=C:/b/s/w/ir/x/w/sdk/pkg/compiler/lib'
+];
+
+const _argsWithSnapshotting = [
+  _autosnapshottingArg,
+  '--sdk=C:/b/s/w/ir/x/w/sdk/sdk/',
+  '--train-using=C:/b/s/w/ir/x/w/sdk/pkg/compiler/lib'
+];
+
+// This constant is referenced in README.md for auto-snapshotting.
+const _autosnapshottingArg =
+    '--autosnapshotting=thresholdMb=200,increaseMb=100,dir=/Users/polinach/Downloads/analyzer_snapshots,dirLimitMb=10000,delaySec=20';