Version 2.18.0-84.0.dev
Merge commit '699ea58a4278192144f9293c091407eaae05cd18' into 'dev'
diff --git a/DEPS b/DEPS
index 8501f0d..0ea6033 100644
--- a/DEPS
+++ b/DEPS
@@ -59,7 +59,7 @@
# Checkout extra javascript engines for testing or benchmarking.
# d8, the V8 shell, is always checked out.
"checkout_javascript_engines": False,
- "d8_tag": "version:10.0.40",
+ "d8_tag": "version:10.2.78",
"jsshell_tag": "version:95.0",
# As Flutter does, we use Fuchsia's GN and Clang toolchain. These revision
@@ -85,7 +85,7 @@
"boringssl_rev": "87f316d7748268eb56f2dc147bd593254ae93198",
"browser-compat-data_tag": "ac8cae697014da1ff7124fba33b0b4245cc6cd1b", # v1.0.22
"browser_launcher_rev": "c6cc1025d6901926cf022e144ba109677e3548f1",
- "characters_rev": "6ec389c4dfa8fce14820dc5cbf6e693202e7e052",
+ "characters_rev": "4b1d4b7737ad47cd2b8105c47e2159174010f29f",
"charcode_rev": "84ea427711e24abf3b832923959caa7dd9a8514b",
"chrome_rev": "19997",
"cli_util_rev": "b0adbba89442b2ea6fef39c7a82fe79cb31e1168",
@@ -138,7 +138,7 @@
"pool_rev": "7abe634002a1ba8a0928eded086062f1307ccfae",
"process_rev": "56ece43b53b64c63ae51ec184b76bd5360c28d0b",
"protobuf_rev": "c1eb6cb51af39ccbaa1a8e19349546586a5c8e31",
- "pub_rev": "a949b329b1b51f5f3973a790e0a0a45897d837de",
+ "pub_rev": "6068f47c264ef790e16411b31b2c94ad6beb1ab6",
"pub_semver_rev": "ea6c54019948dc03042c595ce9413e17aaf7aa38",
"root_certificates_rev": "692f6d6488af68e0121317a9c2c9eb393eb0ee50",
"rust_revision": "b7856f695d65a8ebc846754f97d15814bcb1c244",
diff --git a/pkg/analysis_server/tool/code_completion/completion_metrics.dart b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
index ade5fdc..9f14280 100644
--- a/pkg/analysis_server/tool/code_completion/completion_metrics.dart
+++ b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
@@ -19,7 +19,7 @@
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analysis_server/src/services/completion/dart/utilities.dart';
import 'package:analysis_server/src/status/pages.dart';
-import 'package:analyzer/dart/analysis/context_root.dart';
+import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
@@ -38,22 +38,16 @@
PrefixElement,
TypeParameterElement,
VariableElement;
-import 'package:analyzer/diagnostic/diagnostic.dart';
-import 'package:analyzer/error/error.dart' as err;
import 'package:analyzer/file_system/file_system.dart';
-import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
-import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
-import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer/src/services/available_declarations.dart';
-import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer_plugin/src/utilities/completion/optype.dart';
import 'package:args/args.dart';
-import 'package:cli_util/cli_logging.dart';
import 'package:collection/collection.dart';
+import 'completion_metrics_base.dart';
import 'metrics_util.dart';
import 'output_utilities.dart';
import 'relevance_table_generator.dart';
@@ -96,12 +90,12 @@
return;
}
- var options = CompletionMetricsOptions(result);
+ var options = CompletionMetricsQualityOptions(result);
var provider = PhysicalResourceProvider.INSTANCE;
if (result.wasParsed('reduceDir')) {
var targetMetrics = <CompletionMetrics>[];
var dir = provider.getFolder(result['reduceDir'] as String);
- var computer = CompletionMetricsComputer('', options);
+ var computer = CompletionQualityMetricsComputer('', options);
for (var child in dir.getChildren()) {
if (child is File) {
var metricsList =
@@ -128,7 +122,7 @@
var rootPath = result.rest[0];
print('Analyzing root: "$rootPath"');
var stopwatch = Stopwatch()..start();
- var computer = CompletionMetricsComputer(rootPath, options);
+ var computer = CompletionQualityMetricsComputer(rootPath, options);
await computer.computeMetrics();
stopwatch.stop();
@@ -176,25 +170,25 @@
help: 'The number of characters to include in the prefix. Each '
'completion will be requested this many characters in from the '
'start of the token being completed.')
- ..addFlag(CompletionMetricsOptions.PRINT_MISSED_COMPLETION_DETAILS,
+ ..addFlag(CompletionMetricsQualityOptions.PRINT_MISSED_COMPLETION_DETAILS,
defaultsTo: false,
help:
'Print detailed information every time a completion request fails '
'to produce a suggestions matching the expected suggestion.',
negatable: false)
- ..addFlag(CompletionMetricsOptions.PRINT_MISSED_COMPLETION_SUMMARY,
+ ..addFlag(CompletionMetricsQualityOptions.PRINT_MISSED_COMPLETION_SUMMARY,
defaultsTo: false,
help: 'Print summary information about the times that a completion '
'request failed to produce a suggestions matching the expected '
'suggestion.',
negatable: false)
- ..addFlag(CompletionMetricsOptions.PRINT_MISSING_INFORMATION,
+ ..addFlag(CompletionMetricsQualityOptions.PRINT_MISSING_INFORMATION,
defaultsTo: false,
help: 'Print information about places where no completion location was '
'computed and about information that is missing in the completion '
'tables.',
negatable: false)
- ..addFlag(CompletionMetricsOptions.PRINT_MRR_BY_LOCATION,
+ ..addFlag(CompletionMetricsQualityOptions.PRINT_MRR_BY_LOCATION,
defaultsTo: false,
help:
'Print information about the mrr score achieved at each completion '
@@ -202,7 +196,7 @@
'score by pointing out the locations that are causing the biggest '
'impact.',
negatable: false)
- ..addFlag(CompletionMetricsOptions.PRINT_SHADOWED_COMPLETION_DETAILS,
+ ..addFlag(CompletionMetricsQualityOptions.PRINT_SHADOWED_COMPLETION_DETAILS,
defaultsTo: false,
help: 'Print detailed information every time a completion request '
'produces a suggestion whose name matches the expected suggestion '
@@ -213,7 +207,7 @@
help: 'Print information about the completion requests that were the '
'slowest to return suggestions.',
negatable: false)
- ..addFlag(CompletionMetricsOptions.PRINT_WORST_RESULTS,
+ ..addFlag(CompletionMetricsQualityOptions.PRINT_WORST_RESULTS,
defaultsTo: false,
help: 'Print information about the completion requests that had the '
'worst mrr scores.',
@@ -303,7 +297,7 @@
}
/// A wrapper for the collection of [Counter] and [MeanReciprocalRankComputer]
-/// objects for a run of [CompletionMetricsComputer].
+/// objects for a run of [CompletionQualityMetricsComputer].
class CompletionMetrics {
/// The maximum number of slowest results to collect.
static const maxSlowestResults = 100;
@@ -652,25 +646,122 @@
}
}
+/// The options specified on the command-line.
+class CompletionMetricsQualityOptions extends CompletionMetricsOptions {
+ /// A flag that causes detailed information to be printed every time a
+ /// completion request fails to produce a suggestions matching the expected
+ /// suggestion.
+ static const String PRINT_MISSED_COMPLETION_DETAILS =
+ 'print-missed-completion-details';
+
+ /// A flag that causes summary information to be printed about the times that
+ /// a completion request failed to produce a suggestions matching the expected
+ /// suggestion.
+ static const String PRINT_MISSED_COMPLETION_SUMMARY =
+ 'print-missed-completion-summary';
+
+ /// A flag that causes information to be printed about places where no
+ /// completion location was computed and about information that's missing in
+ /// the completion tables.
+ static const String PRINT_MISSING_INFORMATION = 'print-missing-information';
+
+ /// A flag that causes information to be printed about the mrr score achieved
+ /// at each completion location.
+ static const String PRINT_MRR_BY_LOCATION = 'print-mrr-by-location';
+
+ /// A flag that causes detailed information to be printed every time a
+ /// completion request produce a suggestions whose name matches the expected
+ /// suggestion but that is referencing a different element (one that's
+ /// shadowed by the correct element).
+ static const String PRINT_SHADOWED_COMPLETION_DETAILS =
+ 'print-shadowed-completion-details';
+
+ /// A flag that causes information to be printed about the completion requests
+ /// that had the worst mrr scores.
+ static const String PRINT_WORST_RESULTS = 'print-worst-results';
+
+ /// A flag indicating whether information should be printed every time a
+ /// completion request fails to produce a suggestions matching the expected
+ /// suggestion.
+ final bool printMissedCompletionDetails;
+
+ /// A flag indicating whether information should be printed every time a
+ /// completion request fails to produce a suggestions matching the expected
+ /// suggestion.
+ final bool printMissedCompletionSummary;
+
+ /// A flag indicating whether information should be printed about places where
+ /// no completion location was computed and about information that's missing
+ /// in the completion tables.
+ final bool printMissingInformation;
+
+ /// A flag indicating whether information should be printed about the mrr
+ /// score achieved at each completion location.
+ final bool printMrrByLocation;
+
+ /// A flag indicating whether information should be printed every time a
+ /// completion request fails to produce a suggestions matching the expected
+ /// suggestion.
+ final bool printShadowedCompletionDetails;
+
+ /// A flag indicating whether information should be printed about the
+ /// completion requests that had the worst mrr scores.
+ final bool printWorstResults;
+
+ CompletionMetricsQualityOptions(super.results)
+ : printMissedCompletionDetails =
+ results[PRINT_MISSED_COMPLETION_DETAILS] as bool,
+ printMissedCompletionSummary =
+ results[PRINT_MISSED_COMPLETION_SUMMARY] as bool,
+ printMissingInformation = results[PRINT_MISSING_INFORMATION] as bool,
+ printMrrByLocation = results[PRINT_MRR_BY_LOCATION] as bool,
+ printShadowedCompletionDetails =
+ results[PRINT_SHADOWED_COMPLETION_DETAILS] as bool,
+ printWorstResults = results[PRINT_WORST_RESULTS] as bool;
+}
+
/// This is the main metrics computer class for code completions. After the
/// object is constructed, [computeCompletionMetrics] is executed to do analysis
/// and print a summary of the metrics gathered from the completion tests.
-class CompletionMetricsComputer {
- final String rootPath;
-
- final CompletionMetricsOptions options;
-
- late ResolvedUnitResult _resolvedUnitResult;
-
+class CompletionQualityMetricsComputer extends CompletionMetricsComputer {
/// A list of the metrics to be computed.
final List<CompletionMetrics> targetMetrics = [];
- final OverlayResourceProvider _provider =
- OverlayResourceProvider(PhysicalResourceProvider.INSTANCE);
+ DeclarationsTracker? _declarationsTracker;
- int overlayModificationStamp = 0;
+ protocol.CompletionAvailableSuggestionsParams? _availableSuggestionsParams;
- CompletionMetricsComputer(this.rootPath, this.options);
+ CompletionQualityMetricsComputer(
+ super.rootPath, CompletionMetricsQualityOptions super.options);
+
+ @override
+ CompletionMetricsQualityOptions get options =>
+ super.options as CompletionMetricsQualityOptions;
+
+ @override
+ Future<void> applyOverlay(
+ AnalysisContext context,
+ String filePath,
+ ExpectedCompletion expectedCompletion,
+ ) async {
+ // If an overlay option is being used, compute the overlay file, and
+ // have the context reanalyze the file
+ if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) {
+ var overlayContents = CompletionMetricsComputer.getOverlayContents(
+ resolvedUnitResult.content,
+ expectedCompletion,
+ options.overlay,
+ options.prefixLength);
+
+ provider.setOverlay(filePath,
+ content: overlayContents,
+ modificationStamp: overlayModificationStamp++);
+ context.changeFile(filePath);
+ await context.applyPendingFileChanges();
+ resolvedUnitResult = await context.currentSession
+ .getResolvedUnit(filePath) as ResolvedUnitResult;
+ }
+ }
/// Compare the metrics when each feature is used in isolation.
void compareIndividualFeatures({bool availableSuggestions = false}) {
@@ -713,6 +804,7 @@
}
}
+ @override
Future<void> computeMetrics() async {
// To compare two or more changes to completions, add a `CompletionMetrics`
// object with enable and disable functions to the list of `targetMetrics`.
@@ -734,13 +826,69 @@
// targetMetrics.add(CompletionMetrics('new protocol',
// availableSuggestions: false, useNewProtocol: true));
- final collection = AnalysisContextCollectionImpl(
- includedPaths: [rootPath],
- resourceProvider: PhysicalResourceProvider.INSTANCE,
- );
- for (var context in collection.contexts) {
- await _computeInContext(context.contextRoot);
+ await super.computeMetrics();
+ }
+
+ @override
+ Future<void> computeSuggestionsAndMetrics(
+ ExpectedCompletion expectedCompletion,
+ AnalysisContext context,
+ DocumentationCache documentationCache,
+ ) async {
+ // As this point the completion suggestions are computed,
+ // and results are collected with varying settings for
+ // comparison:
+
+ Future<int> handleExpectedCompletion({
+ required MetricsSuggestionListener listener,
+ required CompletionMetrics metrics,
+ }) async {
+ var stopwatch = Stopwatch()..start();
+ var request = DartCompletionRequest.forResolvedUnit(
+ resolvedUnit: resolvedUnitResult,
+ offset: expectedCompletion.offset,
+ documentationCache: documentationCache,
+ );
+
+ var opType = OpType.forCompletion(request.target, request.offset);
+ var suggestions = await _computeCompletionSuggestions(
+ listener,
+ OperationPerformanceImpl('<root>'),
+ request,
+ metrics.availableSuggestions ? _declarationsTracker : null,
+ metrics.availableSuggestions ? _availableSuggestionsParams : null,
+ metrics.useNewProtocol ? NotImportedSuggestions() : null,
+ );
+ stopwatch.stop();
+
+ return gatherMetricsForSuggestions(
+ request,
+ listener,
+ expectedCompletion,
+ opType.completionLocation,
+ suggestions,
+ metrics,
+ stopwatch.elapsedMilliseconds);
}
+
+ var bestRank = -1;
+ var bestName = '';
+ var defaultTag = getCurrentTag();
+ for (var metrics in targetMetrics) {
+ // Compute the completions.
+ metrics.enable();
+ metrics.userTag.makeCurrent();
+ var listener = MetricsSuggestionListener();
+ var rank =
+ await handleExpectedCompletion(listener: listener, metrics: metrics);
+ if (bestRank < 0 || rank < bestRank) {
+ bestRank = rank;
+ bestName = metrics.name;
+ }
+ defaultTag.makeCurrent();
+ metrics.disable();
+ }
+ rankComparison.count(bestName);
}
/// Gathers various metrics for the completion [request] which resulted in
@@ -1247,6 +1395,32 @@
}
}
+ @override
+ void removeOverlay(String filePath) {
+ // If an overlay option is being used, remove the overlay applied
+ // earlier.
+ if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) {
+ provider.removeOverlay(filePath);
+ }
+ }
+
+ @override
+ void setupForResolution(AnalysisContext context) {
+ if (targetMetrics.any((metrics) => metrics.availableSuggestions)) {
+ var declarationsTracker = DeclarationsTracker(
+ MemoryByteStore(), PhysicalResourceProvider.INSTANCE);
+ declarationsTracker.addContext(context);
+ while (declarationsTracker.hasWork) {
+ declarationsTracker.doWork();
+ }
+
+ // Have the [AvailableDeclarationsSet] computed to use later.
+ _availableSuggestionsParams = createCompletionAvailableSuggestions(
+ declarationsTracker.allLibraries.toList(), []);
+ _declarationsTracker = declarationsTracker;
+ }
+ }
+
int _computeCharsBeforeTop(ExpectedCompletion target,
List<protocol.CompletionSuggestion> suggestions,
{int minRank = 1}) {
@@ -1358,172 +1532,6 @@
return suggestions;
}
- /// Compute the metrics for the files in the context [root], creating a
- /// separate context collection to prevent accumulating memory. The metrics
- /// should be captured in the [collector].
- Future<void> _computeInContext(ContextRoot root) async {
- // Create a new collection to avoid consuming large quantities of memory.
- final collection = AnalysisContextCollectionImpl(
- includedPaths: root.includedPaths.toList(),
- excludedPaths: root.excludedPaths.toList(),
- resourceProvider: _provider,
- );
-
- var context = collection.contexts[0];
-
- DeclarationsTracker? declarationsTracker;
- protocol.CompletionAvailableSuggestionsParams? availableSuggestionsParams;
- if (targetMetrics.any((metrics) => metrics.availableSuggestions)) {
- declarationsTracker = DeclarationsTracker(
- MemoryByteStore(), PhysicalResourceProvider.INSTANCE);
- declarationsTracker.addContext(context);
- while (declarationsTracker.hasWork) {
- declarationsTracker.doWork();
- }
-
- // Have the [AvailableDeclarationsSet] computed to use later.
- availableSuggestionsParams = createCompletionAvailableSuggestions(
- declarationsTracker.allLibraries.toList(), []);
- }
-
- // Loop through each file, resolve the file and call
- // [forEachExpectedCompletion].
-
- var ansi = Ansi(Ansi.terminalSupportsAnsi);
- var logger = Logger.standard(ansi: ansi);
- var analyzedFileCount = context.contextRoot.analyzedFiles().length;
- logger.write('Computing completions at root: ${root.root.path} '
- '($analyzedFileCount files)\n');
-
- logger.write('Resolving...\n');
- var progress = _ProgressBar(logger, analyzedFileCount);
-
- var dartdocDirectiveInfo = DartdocDirectiveInfo();
- var documentationCache = DocumentationCache(dartdocDirectiveInfo);
- var results = <ResolvedUnitResult>[];
- var pathContext = context.contextRoot.resourceProvider.pathContext;
- for (var filePath in context.contextRoot.analyzedFiles()) {
- if (file_paths.isDart(pathContext, filePath)) {
- try {
- var result = await context.currentSession.getResolvedUnit(filePath)
- as ResolvedUnitResult;
-
- var analysisError = getFirstErrorOrNull(result);
- if (analysisError != null) {
- progress.clear();
- print('File $filePath skipped due to errors such as:');
- print(' ${analysisError.toString()}');
- print('');
- continue;
- } else {
- results.add(result);
- documentationCache.cacheFromResult(result);
- }
- } catch (exception, stackTrace) {
- progress.clear();
- print('Exception caught analyzing: $filePath');
- print(exception.toString());
- print(stackTrace);
- }
- }
- progress.tick();
- }
- progress.complete();
-
- logger.write('Analyzing completion suggestions...\n');
- progress = _ProgressBar(logger, results.length);
- for (var result in results) {
- _resolvedUnitResult = result;
- var filePath = result.path;
- // Use the ExpectedCompletionsVisitor to compute the set of expected
- // completions for this CompilationUnit.
- final visitor =
- ExpectedCompletionsVisitor(result, caretOffset: options.prefixLength);
- _resolvedUnitResult.unit.accept(visitor);
-
- for (var expectedCompletion in visitor.expectedCompletions) {
- var resolvedUnitResult = _resolvedUnitResult;
-
- // If an overlay option is being used, compute the overlay file, and
- // have the context reanalyze the file
- if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) {
- var overlayContents = _getOverlayContents(
- _resolvedUnitResult.content, expectedCompletion);
-
- _provider.setOverlay(filePath,
- content: overlayContents,
- modificationStamp: overlayModificationStamp++);
- context.changeFile(filePath);
- await context.applyPendingFileChanges();
- resolvedUnitResult = await context.currentSession
- .getResolvedUnit(filePath) as ResolvedUnitResult;
- }
-
- // As this point the completion suggestions are computed,
- // and results are collected with varying settings for
- // comparison:
-
- Future<int> handleExpectedCompletion(
- {required MetricsSuggestionListener listener,
- required CompletionMetrics metrics}) async {
- var stopwatch = Stopwatch()..start();
- var request = DartCompletionRequest.forResolvedUnit(
- resolvedUnit: resolvedUnitResult,
- offset: expectedCompletion.offset,
- documentationCache: documentationCache,
- );
-
- var opType = OpType.forCompletion(request.target, request.offset);
- var suggestions = await _computeCompletionSuggestions(
- listener,
- OperationPerformanceImpl('<root>'),
- request,
- metrics.availableSuggestions ? declarationsTracker : null,
- metrics.availableSuggestions ? availableSuggestionsParams : null,
- metrics.useNewProtocol ? NotImportedSuggestions() : null,
- );
- stopwatch.stop();
-
- return gatherMetricsForSuggestions(
- request,
- listener,
- expectedCompletion,
- opType.completionLocation,
- suggestions,
- metrics,
- stopwatch.elapsedMilliseconds);
- }
-
- var bestRank = -1;
- var bestName = '';
- var defaultTag = getCurrentTag();
- for (var metrics in targetMetrics) {
- // Compute the completions.
- metrics.enable();
- metrics.userTag.makeCurrent();
- var listener = MetricsSuggestionListener();
- var rank = await handleExpectedCompletion(
- listener: listener, metrics: metrics);
- if (bestRank < 0 || rank < bestRank) {
- bestRank = rank;
- bestName = metrics.name;
- }
- defaultTag.makeCurrent();
- metrics.disable();
- }
- rankComparison.count(bestName);
-
- // If an overlay option is being used, remove the overlay applied
- // earlier.
- if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) {
- _provider.removeOverlay(filePath);
- }
- }
- progress.tick();
- }
- progress.complete();
- }
-
List<protocol.CompletionSuggestion> _filterSuggestions(
String prefix, List<protocol.CompletionSuggestion> suggestions) {
// TODO(brianwilkerson) Replace this with a more realistic filtering
@@ -1533,32 +1541,6 @@
.toList();
}
- String _getOverlayContents(
- String contents, ExpectedCompletion expectedCompletion) {
- assert(contents.isNotEmpty);
- var offset = expectedCompletion.offset;
- var length = expectedCompletion.syntacticEntity.length;
- assert(offset >= 0);
- assert(length > 0);
- var tokenEndOffset = offset + length;
- if (length >= options.prefixLength) {
- // Rather than removing the whole token, remove the characters after
- // the given prefix length.
- offset += options.prefixLength;
- }
- if (options.overlay == CompletionMetricsOptions.OVERLAY_REMOVE_TOKEN) {
- return contents.substring(0, offset) + contents.substring(tokenEndOffset);
- } else if (options.overlay ==
- CompletionMetricsOptions.OVERLAY_REMOVE_REST_OF_FILE) {
- return contents.substring(0, offset);
- } else {
- var removeToken = CompletionMetricsOptions.OVERLAY_REMOVE_TOKEN;
- var removeRest = CompletionMetricsOptions.OVERLAY_REMOVE_REST_OF_FILE;
- throw Exception('\'_getOverlayContents\' called with option other than'
- '$removeToken and $removeRest: ${options.overlay}');
- }
- }
-
void _printWorstResults(String title, List<CompletionResult> worstResults) {
List<String> suggestionRow(int rank, SuggestionData data) {
var suggestion = data.suggestion;
@@ -1641,18 +1623,6 @@
}
}
- /// Given some [ResolvedUnitResult] return the first error of high severity
- /// if such an error exists, `null` otherwise.
- static err.AnalysisError? getFirstErrorOrNull(
- ResolvedUnitResult resolvedUnitResult) {
- for (var error in resolvedUnitResult.errors) {
- if (error.severity == Severity.error) {
- return error;
- }
- }
- return null;
- }
-
/// Returns a [Place] indicating the position of [expectedCompletion] in
/// [suggestions].
///
@@ -1669,132 +1639,6 @@
}
}
-/// The options specified on the command-line.
-class CompletionMetricsOptions {
- /// An option to control whether and how overlays should be produced.
- static const String OVERLAY = 'overlay';
-
- /// A mode indicating that no overlays should be produced.
- static const String OVERLAY_NONE = 'none';
-
- /// A mode indicating that everything from the completion offset to the end of
- /// the file should be removed.
- static const String OVERLAY_REMOVE_REST_OF_FILE = 'remove-rest-of-file';
-
- /// A mode indicating that the token whose offset is the same as the
- /// completion offset should be removed.
- static const String OVERLAY_REMOVE_TOKEN = 'remove-token';
-
- /// An option controlling how long of a prefix should be used.
- ///
- /// This affects the offset of the completion request, and how much content is
- /// removed in each of the overlay modes.
- static const String PREFIX_LENGTH = 'prefix-length';
-
- /// A flag that causes detailed information to be printed every time a
- /// completion request fails to produce a suggestions matching the expected
- /// suggestion.
- static const String PRINT_MISSED_COMPLETION_DETAILS =
- 'print-missed-completion-details';
-
- /// A flag that causes summary information to be printed about the times that
- /// a completion request failed to produce a suggestions matching the expected
- /// suggestion.
- static const String PRINT_MISSED_COMPLETION_SUMMARY =
- 'print-missed-completion-summary';
-
- /// A flag that causes information to be printed about places where no
- /// completion location was computed and about information that's missing in
- /// the completion tables.
- static const String PRINT_MISSING_INFORMATION = 'print-missing-information';
-
- /// A flag that causes information to be printed about the mrr score achieved
- /// at each completion location.
- static const String PRINT_MRR_BY_LOCATION = 'print-mrr-by-location';
-
- /// A flag that causes detailed information to be printed every time a
- /// completion request produce a suggestions whose name matches the expected
- /// suggestion but that is referencing a different element (one that's
- /// shadowed by the correct element).
- static const String PRINT_SHADOWED_COMPLETION_DETAILS =
- 'print-shadowed-completion-details';
-
- /// A flag that causes information to be printed about the completion requests
- /// that were the slowest to return suggestions.
- static const String PRINT_SLOWEST_RESULTS = 'print-slowest-results';
-
- /// A flag that causes information to be printed about the completion requests
- /// that had the worst mrr scores.
- static const String PRINT_WORST_RESULTS = 'print-worst-results';
-
- /// The overlay mode that should be used.
- final String overlay;
-
- final int prefixLength;
-
- /// A flag indicating whether information should be printed every time a
- /// completion request fails to produce a suggestions matching the expected
- /// suggestion.
- final bool printMissedCompletionDetails;
-
- /// A flag indicating whether information should be printed every time a
- /// completion request fails to produce a suggestions matching the expected
- /// suggestion.
- final bool printMissedCompletionSummary;
-
- /// A flag indicating whether information should be printed about places where
- /// no completion location was computed and about information that's missing
- /// in the completion tables.
- final bool printMissingInformation;
-
- /// A flag indicating whether information should be printed about the mrr
- /// score achieved at each completion location.
- final bool printMrrByLocation;
-
- /// A flag indicating whether information should be printed every time a
- /// completion request fails to produce a suggestions matching the expected
- /// suggestion.
- final bool printShadowedCompletionDetails;
-
- /// A flag indicating whether information should be printed about the
- /// completion requests that were the slowest to return suggestions.
- final bool printSlowestResults;
-
- /// A flag indicating whether information should be printed about the
- /// completion requests that had the worst mrr scores.
- final bool printWorstResults;
-
- factory CompletionMetricsOptions(results) {
- return CompletionMetricsOptions._(
- overlay: results[OVERLAY] as String,
- prefixLength: int.parse(results[PREFIX_LENGTH] as String),
- printMissedCompletionDetails:
- results[PRINT_MISSED_COMPLETION_DETAILS] as bool,
- printMissedCompletionSummary:
- results[PRINT_MISSED_COMPLETION_SUMMARY] as bool,
- printMissingInformation: results[PRINT_MISSING_INFORMATION] as bool,
- printMrrByLocation: results[PRINT_MRR_BY_LOCATION] as bool,
- printShadowedCompletionDetails:
- results[PRINT_SHADOWED_COMPLETION_DETAILS] as bool,
- printSlowestResults: results[PRINT_SLOWEST_RESULTS] as bool,
- printWorstResults: results[PRINT_WORST_RESULTS] as bool);
- }
-
- CompletionMetricsOptions._(
- {required this.overlay,
- required this.prefixLength,
- required this.printMissedCompletionDetails,
- required this.printMissedCompletionSummary,
- required this.printMissingInformation,
- required this.printMrrByLocation,
- required this.printShadowedCompletionDetails,
- required this.printSlowestResults,
- required this.printWorstResults})
- : assert(overlay == OVERLAY_NONE ||
- overlay == OVERLAY_REMOVE_TOKEN ||
- overlay == OVERLAY_REMOVE_REST_OF_FILE);
-}
-
/// The result of a single completion.
class CompletionResult {
final Place place;
@@ -2121,84 +1965,6 @@
}
}
-/// A facility for drawing a progress bar in the terminal.
-///
-/// The bar is instantiated with the total number of "ticks" to be completed,
-/// and progress is made by calling [tick]. The bar is drawn across one entire
-/// line, like so:
-///
-/// [---------- ]
-///
-/// The hyphens represent completed progress, and the whitespace represents
-/// remaining progress.
-///
-/// If there is no terminal, the progress bar will not be drawn.
-class _ProgressBar {
- /// Whether the progress bar should be drawn.
- late bool _shouldDrawProgress;
-
- /// The width of the terminal, in terms of characters.
- late int _width;
-
- final Logger _logger;
-
- /// The inner width of the terminal, in terms of characters.
- ///
- /// This represents the number of characters available for drawing progress.
- late int _innerWidth;
-
- final int _totalTickCount;
-
- int _tickCount = 0;
-
- _ProgressBar(this._logger, this._totalTickCount) {
- if (!stdout.hasTerminal) {
- _shouldDrawProgress = false;
- } else {
- _shouldDrawProgress = true;
- _width = stdout.terminalColumns;
- // Inclusion of the percent indicator assumes a terminal width of at least
- // 12 (2 brackets + 1 space + 2 parenthesis characters + 3 digits +
- // 1 period + 2 digits + 1 '%' character).
- _innerWidth = stdout.terminalColumns - 12;
- _logger.write('[${' ' * _innerWidth}]');
- }
- }
-
- /// Clears the progress bar from the terminal, allowing other logging to be
- /// printed.
- void clear() {
- if (!_shouldDrawProgress) {
- return;
- }
- _logger.write('\r${' ' * _width}\r');
- }
-
- /// Draws the progress bar as complete, and print two newlines.
- void complete() {
- if (!_shouldDrawProgress) {
- return;
- }
- _logger.write('\r[${'-' * _innerWidth}]\n\n');
- }
-
- /// Progresses the bar by one tick.
- void tick() {
- if (!_shouldDrawProgress) {
- return;
- }
- _tickCount++;
- var fractionComplete =
- math.max(0, _tickCount * _innerWidth ~/ _totalTickCount - 1);
- // The inner space consists of hyphens, one spinner character, and spaces.
- var remaining = _innerWidth - fractionComplete - 1;
- var spinner = AnsiProgress.kAnimationItems[_tickCount % 4];
- var pctComplete = (_tickCount * 100 / _totalTickCount).toStringAsFixed(2);
- _logger.write(
- '\r[${'-' * fractionComplete}$spinner${' ' * remaining}] ($pctComplete%)');
- }
-}
-
extension on CompletionGroup {
String get name {
switch (this) {
diff --git a/pkg/analysis_server/tool/code_completion/completion_metrics_base.dart b/pkg/analysis_server/tool/code_completion/completion_metrics_base.dart
new file mode 100644
index 0000000..24cf9c6
--- /dev/null
+++ b/pkg/analysis_server/tool/code_completion/completion_metrics_base.dart
@@ -0,0 +1,333 @@
+// Copyright (c) 2022, 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:io' show stdout;
+import 'dart:math' as math;
+
+import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
+import 'package:analyzer/dart/analysis/analysis_context.dart';
+import 'package:analyzer/dart/analysis/context_root.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/diagnostic/diagnostic.dart';
+import 'package:analyzer/error/error.dart' as err;
+import 'package:analyzer/file_system/overlay_file_system.dart';
+import 'package:analyzer/file_system/physical_file_system.dart';
+import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
+import 'package:analyzer/src/util/file_paths.dart' as file_paths;
+import 'package:args/args.dart';
+import 'package:cli_util/cli_logging.dart';
+
+import 'visitors.dart';
+
+final logger = Logger.standard(ansi: Ansi(Ansi.terminalSupportsAnsi));
+
+/// This is the main metrics computer class for code completions. After the
+/// object is constructed, [computeCompletionMetrics] is executed to do analysis
+/// and print a summary of the metrics gathered from the completion tests.
+abstract class CompletionMetricsComputer {
+ final String rootPath;
+
+ final CompletionMetricsOptions options;
+
+ late ResolvedUnitResult resolvedUnitResult;
+
+ final OverlayResourceProvider provider =
+ OverlayResourceProvider(PhysicalResourceProvider.INSTANCE);
+
+ int overlayModificationStamp = 0;
+
+ CompletionMetricsComputer(this.rootPath, this.options);
+
+ /// Applies an overlay in [filePath] at [expectedCompletion].
+ Future<void> applyOverlay(AnalysisContext context, String filePath,
+ ExpectedCompletion expectedCompletion);
+
+ /// Compute the metrics for the files in the context [root], creating a
+ /// separate context collection to prevent accumulating memory. The metrics
+ /// should be captured in the [collector].
+ Future<void> computeInContext(ContextRoot root) async {
+ // Create a new collection to avoid consuming large quantities of memory.
+ final collection = AnalysisContextCollectionImpl(
+ includedPaths: root.includedPaths.toList(),
+ excludedPaths: root.excludedPaths.toList(),
+ resourceProvider: provider,
+ );
+
+ var context = collection.contexts[0];
+
+ setupForResolution(context);
+
+ logger.write('Computing completions at root: ${root.root.path}\n');
+ var documentationCache = DocumentationCache(DartdocDirectiveInfo());
+
+ var results = await resolveAnalyzedFiles(
+ context: context,
+ documentationCache: documentationCache,
+ );
+
+ logger.write('Analyzing completion suggestions...\n');
+ var progress = ProgressBar(logger, results.length);
+ for (var result in results) {
+ resolvedUnitResult = result;
+ var filePath = result.path;
+ // Use the ExpectedCompletionsVisitor to compute the set of expected
+ // completions for this CompilationUnit.
+ final visitor =
+ ExpectedCompletionsVisitor(result, caretOffset: options.prefixLength);
+ resolvedUnitResult.unit.accept(visitor);
+
+ for (var expectedCompletion in visitor.expectedCompletions) {
+ await applyOverlay(context, filePath, expectedCompletion);
+
+ await computeSuggestionsAndMetrics(
+ expectedCompletion,
+ context,
+ documentationCache,
+ );
+
+ removeOverlay(filePath);
+ }
+ progress.tick();
+ }
+ progress.complete();
+ }
+
+ Future<void> computeMetrics() async {
+ final collection = AnalysisContextCollectionImpl(
+ includedPaths: [rootPath],
+ resourceProvider: PhysicalResourceProvider.INSTANCE,
+ );
+ for (final context in collection.contexts) {
+ await computeInContext(context.contextRoot);
+ }
+ }
+
+ /// Computes suggestions for [expectedCompletion] and computes metrics from
+ /// the resulting suggestions.
+ Future<void> computeSuggestionsAndMetrics(
+ ExpectedCompletion expectedCompletion,
+ AnalysisContext context,
+ DocumentationCache documentationCache,
+ );
+
+ /// Removes the overlay which has been applied to [filePath].
+ void removeOverlay(String filePath);
+
+ /// Resolves all analyzed files within [context].
+ Future<List<ResolvedUnitResult>> resolveAnalyzedFiles({
+ required AnalysisContext context,
+ required DocumentationCache documentationCache,
+ }) async {
+ final analyzedFileCount = context.contextRoot.analyzedFiles().length;
+ logger.write('Resolving $analyzedFileCount files...\n');
+
+ final progress = ProgressBar(logger, analyzedFileCount);
+ final results = <ResolvedUnitResult>[];
+ final pathContext = context.contextRoot.resourceProvider.pathContext;
+ for (final filePath in context.contextRoot.analyzedFiles()) {
+ if (file_paths.isDart(pathContext, filePath)) {
+ try {
+ final result = await context.currentSession.getResolvedUnit(filePath)
+ as ResolvedUnitResult;
+
+ final analysisError = getFirstErrorOrNull(result);
+ if (analysisError != null) {
+ progress.clear();
+ print('File $filePath skipped due to errors such as:');
+ print(' ${analysisError.toString()}');
+ print('');
+ continue;
+ } else {
+ results.add(result);
+ documentationCache.cacheFromResult(result);
+ }
+ } catch (exception, stackTrace) {
+ progress.clear();
+ print('Exception caught analyzing: $filePath');
+ print(exception.toString());
+ print(stackTrace);
+ }
+ }
+ progress.tick();
+ }
+ progress.complete();
+ return results;
+ }
+
+ /// Performs setup tasks with [context] before resolution.
+ void setupForResolution(AnalysisContext context);
+
+ /// Given some [ResolvedUnitResult] returns the first error of high severity
+ /// if such an error exists, `null` otherwise.
+ static err.AnalysisError? getFirstErrorOrNull(
+ ResolvedUnitResult resolvedUnitResult) {
+ for (final error in resolvedUnitResult.errors) {
+ if (error.severity == Severity.error) {
+ return error;
+ }
+ }
+ return null;
+ }
+
+ /// Gets overlay contents for [contents], applying a change at
+ /// [expectedCompletion] with [prefixLength], according to [overlay], one of
+ /// the [CompletionMetricsOptions].
+ static String getOverlayContents(
+ String contents,
+ ExpectedCompletion expectedCompletion,
+ // TODO(srawlins): Replace this with an enum.
+ String overlay,
+ int prefixLength,
+ ) {
+ assert(contents.isNotEmpty);
+ var offset = expectedCompletion.offset;
+ final length = expectedCompletion.syntacticEntity.length;
+ assert(offset >= 0);
+ assert(length > 0);
+ var tokenEndOffset = offset + length;
+ if (length >= prefixLength) {
+ // Rather than removing the whole token, remove the characters after
+ // the given prefix length.
+ offset += prefixLength;
+ }
+ if (overlay == CompletionMetricsOptions.OVERLAY_REMOVE_TOKEN) {
+ return contents.substring(0, offset) + contents.substring(tokenEndOffset);
+ } else if (overlay ==
+ CompletionMetricsOptions.OVERLAY_REMOVE_REST_OF_FILE) {
+ return contents.substring(0, offset);
+ } else {
+ final removeToken = CompletionMetricsOptions.OVERLAY_REMOVE_TOKEN;
+ final removeRest = CompletionMetricsOptions.OVERLAY_REMOVE_REST_OF_FILE;
+ throw Exception('\'getOverlayContents\' called with option other than'
+ '$removeToken and $removeRest: $overlay');
+ }
+ }
+}
+
+/// The options specified on the command-line.
+class CompletionMetricsOptions {
+ /// An option to control whether and how overlays should be produced.
+ static const String OVERLAY = 'overlay';
+
+ /// A mode indicating that no overlays should be produced.
+ /// TODO(srawlins): Replace this and the other two overlay values with enums.
+ static const String OVERLAY_NONE = 'none';
+
+ /// A mode indicating that everything from the completion offset to the end of
+ /// the file should be removed.
+ static const String OVERLAY_REMOVE_REST_OF_FILE = 'remove-rest-of-file';
+
+ /// A mode indicating that the token whose offset is the same as the
+ /// completion offset should be removed.
+ static const String OVERLAY_REMOVE_TOKEN = 'remove-token';
+
+ /// An option controlling how long of a prefix should be used.
+ ///
+ /// This affects the offset of the completion request, and how much content is
+ /// removed in each of the overlay modes.
+ static const String PREFIX_LENGTH = 'prefix-length';
+
+ /// A flag that causes information to be printed about the completion requests
+ /// that were the slowest to return suggestions.
+ static const String PRINT_SLOWEST_RESULTS = 'print-slowest-results';
+
+ /// The overlay mode that should be used.
+ /// TODO(srawlins): Replace this with an enum.
+ final String overlay;
+
+ final int prefixLength;
+
+ /// A flag indicating whether information should be printed about the
+ /// completion requests that were the slowest to return suggestions.
+ final bool printSlowestResults;
+
+ CompletionMetricsOptions(ArgResults results)
+ : overlay = results[OVERLAY] as String,
+ prefixLength = int.parse(results[PREFIX_LENGTH] as String),
+ printSlowestResults = results[PRINT_SLOWEST_RESULTS] as bool {
+ assert(overlay == OVERLAY_NONE ||
+ overlay == OVERLAY_REMOVE_TOKEN ||
+ overlay == OVERLAY_REMOVE_REST_OF_FILE);
+ }
+}
+
+/// A facility for drawing a progress bar in the terminal.
+///
+/// The bar is instantiated with the total number of "ticks" to be completed,
+/// and progress is made by calling [tick]. The bar is drawn across one entire
+/// line, like so:
+///
+/// [---------- ]
+///
+/// The hyphens represent completed progress, and the whitespace represents
+/// remaining progress.
+///
+/// If there is no terminal, the progress bar will not be drawn.
+class ProgressBar {
+ /// Whether the progress bar should be drawn.
+ late bool _shouldDrawProgress;
+
+ /// The width of the terminal, in terms of characters.
+ late int _width;
+
+ final Logger _logger;
+
+ /// The inner width of the terminal, in terms of characters.
+ ///
+ /// This represents the number of characters available for drawing progress.
+ late int _innerWidth;
+
+ final int _totalTickCount;
+
+ int _tickCount = 0;
+
+ ProgressBar(this._logger, this._totalTickCount) {
+ if (!stdout.hasTerminal) {
+ _shouldDrawProgress = false;
+ } else {
+ _shouldDrawProgress = true;
+ _width = stdout.terminalColumns;
+ // Inclusion of the percent indicator assumes a terminal width of at least
+ // 12 (2 brackets + 1 space + 2 parenthesis characters + 3 digits +
+ // 1 period + 2 digits + 1 '%' character).
+ _innerWidth = stdout.terminalColumns - 12;
+ _logger.write('[${' ' * _innerWidth}]');
+ }
+ }
+
+ /// Clears the progress bar from the terminal, allowing other logging to be
+ /// printed.
+ void clear() {
+ if (!_shouldDrawProgress) {
+ return;
+ }
+ _logger.write('\r${' ' * _width}\r');
+ }
+
+ /// Draws the progress bar as complete, and print two newlines.
+ void complete() {
+ if (!_shouldDrawProgress) {
+ return;
+ }
+ _logger.write('\r[${'-' * _innerWidth}]\n\n');
+ }
+
+ /// Progresses the bar by one tick.
+ void tick() {
+ if (!_shouldDrawProgress) {
+ return;
+ }
+ _tickCount++;
+ final fractionComplete =
+ math.max(0, _tickCount * _innerWidth ~/ _totalTickCount - 1);
+ // The inner space consists of hyphens, one spinner character, spaces, and a
+ // percentage (8 characters).
+ final hyphens = '-' * fractionComplete;
+ final trailingSpace = ' ' * (_innerWidth - fractionComplete - 1);
+ final spinner = AnsiProgress.kAnimationItems[_tickCount % 4];
+ final pctComplete = (_tickCount * 100 / _totalTickCount).toStringAsFixed(2);
+ _logger.write('\r[$hyphens$spinner$trailingSpace] ($pctComplete%)');
+ }
+}
diff --git a/pkg/analysis_server/tool/code_completion/completion_metrics_client.dart b/pkg/analysis_server/tool/code_completion/completion_metrics_client.dart
new file mode 100644
index 0000000..2248b30
--- /dev/null
+++ b/pkg/analysis_server/tool/code_completion/completion_metrics_client.dart
@@ -0,0 +1,592 @@
+// Copyright (c) 2022, 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 'dart:convert';
+import 'dart:io';
+
+import 'package:analysis_server/protocol/protocol_generated.dart';
+import 'package:analysis_server/src/protocol/protocol_internal.dart';
+import 'package:analysis_server/src/server/driver.dart';
+import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
+import 'package:analyzer/dart/analysis/analysis_context.dart';
+import 'package:args/args.dart';
+import 'package:path/path.dart' as path;
+
+import 'completion_metrics_base.dart';
+import 'metrics_util.dart';
+import 'output_utilities.dart';
+import 'relevance_table_generator.dart';
+import 'visitors.dart';
+
+Future<void> main(List<String> args) async {
+ var parser = _createArgParser();
+ var result = parser.parse(args);
+
+ if (!_validArguments(parser, result)) {
+ return;
+ }
+
+ var rootPath = result.rest[0];
+ final targets = <Directory>[];
+ if (Directory(rootPath).existsSync()) {
+ targets.add(Directory(rootPath));
+ } else {
+ throw "Directory doesn't exist: $rootPath";
+ }
+
+ var options = CompletionMetricsOptions(result);
+ var stopwatch = Stopwatch()..start();
+ var client = _AnalysisServerClient(Directory(_sdk.sdkPath), targets);
+ CompletionClientMetricsComputer(rootPath, options, client).computeMetrics();
+ stopwatch.stop();
+
+ var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
+ print('');
+ print('Metrics computed in $duration');
+}
+
+final _Sdk _sdk = _Sdk._instance;
+
+/// Given a data structure which is a Map of String to dynamic values, returns
+/// the same structure (`Map<String, dynamic>`) with the correct runtime types.
+Map<String, dynamic> _castStringKeyedMap(dynamic untyped) {
+ final Map<dynamic, dynamic> map = untyped! as Map<dynamic, dynamic>;
+ return map.cast<String, dynamic>();
+}
+
+/// Creates a parser that can be used to parse the command-line arguments.
+ArgParser _createArgParser() {
+ return ArgParser(
+ usageLineLength: stdout.hasTerminal ? stdout.terminalColumns : 80)
+ ..addFlag(
+ 'help',
+ abbr: 'h',
+ help: 'Print this help message.',
+ )
+ ..addOption(
+ CompletionMetricsOptions.OVERLAY,
+ allowed: [
+ CompletionMetricsOptions.OVERLAY_NONE,
+ CompletionMetricsOptions.OVERLAY_REMOVE_TOKEN,
+ CompletionMetricsOptions.OVERLAY_REMOVE_REST_OF_FILE,
+ ],
+ defaultsTo: CompletionMetricsOptions.OVERLAY_NONE,
+ help: 'Before attempting a completion at the location of each token, the '
+ 'token can be removed, or the rest of the file can be removed to '
+ 'test code completion with diverse methods. The default mode is to '
+ 'complete at the start of the token without modifying the file.',
+ )
+ ..addOption(
+ CompletionMetricsOptions.PREFIX_LENGTH,
+ defaultsTo: '0',
+ help: 'The number of characters to include in the prefix. Each '
+ 'completion will be requested this many characters in from the '
+ 'start of the token being completed.',
+ )
+ ..addFlag(
+ CompletionMetricsOptions.PRINT_SLOWEST_RESULTS,
+ defaultsTo: false,
+ help: 'Print information about the completion requests that were the '
+ 'slowest to return suggestions.',
+ negatable: false,
+ );
+}
+
+/// Prints usage information for this tool.
+void _printUsage(ArgParser parser, {String? error}) {
+ if (error != null) {
+ print(error);
+ print('');
+ }
+ print('usage: dart completion_metrics_client.dart [options] packagePath');
+ print('');
+ print('Compute code completion health metrics.');
+ print('');
+ print(parser.usage);
+}
+
+/// Trims [suffix] from the end of [text].
+String _trimEnd(String text, String suffix) {
+ if (text.endsWith(suffix)) {
+ return text.substring(0, text.length - suffix.length);
+ }
+ return text;
+}
+
+/// Returns `true` if the command-line arguments (represented by the [result]
+/// and parsed by the [parser]) are valid.
+bool _validArguments(ArgParser parser, ArgResults result) {
+ if (result.wasParsed('help')) {
+ _printUsage(parser);
+ return false;
+ } else if (result.rest.length != 1) {
+ _printUsage(parser, error: 'No package path specified.');
+ return false;
+ }
+ return validateDir(parser, result.rest[0]);
+}
+
+class CompletionClientMetricsComputer extends CompletionMetricsComputer {
+ final _AnalysisServerClient client;
+
+ final CompletionMetrics targetMetric = CompletionMetrics();
+
+ final metrics = CompletionMetrics();
+
+ CompletionClientMetricsComputer(super.rootPath, super.options, this.client);
+
+ @override
+ Future<void> applyOverlay(
+ AnalysisContext context,
+ String filePath,
+ ExpectedCompletion expectedCompletion,
+ ) async {
+ // TODO(srawlins): Support overlays.
+ }
+
+ @override
+ Future<void> computeMetrics() async {
+ await client.start();
+ await super.computeMetrics();
+ await client.shutdown();
+
+ // A row containing the name, median, p90, and p95 scores in [computer].
+ List<String> m9095Row(PercentileComputer computer) => [
+ computer.name,
+ computer.median.toString(),
+ computer.p90.toString(),
+ computer.p95.toString(),
+ ];
+
+ var table = [
+ ['', 'median', 'p90', 'p95'],
+ m9095Row(metrics.totalPercentileComputer),
+ m9095Row(metrics.requestResponsePercentileComputer),
+ m9095Row(metrics.decodePercentileComputer),
+ m9095Row(metrics.deserializePercentileComputer),
+ ];
+
+ rightJustifyColumns(table, range(1, table[0].length));
+ printTable(table);
+ }
+
+ @override
+ Future<void> computeSuggestionsAndMetrics(
+ ExpectedCompletion expectedCompletion,
+ AnalysisContext context,
+ DocumentationCache documentationCache,
+ ) async {
+ var stopwatch = Stopwatch()..start();
+ var suggestionsData = await client.requestCompletion(
+ expectedCompletion.filePath, expectedCompletion.offset, 1000);
+ stopwatch.stop();
+ var metadata = suggestionsData.metadata;
+
+ metrics.totalPercentileComputer.addValue(stopwatch.elapsedMilliseconds);
+ metrics.requestResponsePercentileComputer
+ .addValue(metadata.requestResponseDuration);
+ metrics.decodePercentileComputer.addValue(metadata.decodeDuration);
+ metrics.deserializePercentileComputer
+ .addValue(metadata.deserializeDuration);
+ }
+
+ @override
+ void removeOverlay(String filePath) {
+ // TODO(srawlins): Support overlays.
+ }
+
+ @override
+ void setupForResolution(AnalysisContext context) {}
+}
+
+class CompletionMetrics {
+ /// A percentile computer which tracks the total time to create and send a
+ /// completion request, and receive and decode a completion response, using
+ /// 2.000 seconds as the max value to use in percentile calculations.
+ final PercentileComputer totalPercentileComputer =
+ PercentileComputer('ms for total duration', valueLimit: 2000);
+
+ /// A percentile computer which tracks the time to send a completion request,
+ /// and receive a completion response, not including any time to encode or
+ /// decode, using 2.000 seconds as the max value to use in percentile
+ /// calculations.
+ final PercentileComputer requestResponsePercentileComputer =
+ PercentileComputer('ms for request/response duration', valueLimit: 2000);
+
+ /// A percentile computer which tracks the time to decode each completion
+ /// response into JSON, using 2.000 seconds as the max value to use in
+ /// percentile calculations.
+ final PercentileComputer decodePercentileComputer =
+ PercentileComputer('ms for decode duration', valueLimit: 2000);
+
+ /// A percentile computer which tracks the time to deserialize each completion
+ /// response JSON into Dart objects, using 2.000 seconds as the max value to
+ /// use in percentile calculations.
+ final PercentileComputer deserializePercentileComputer =
+ PercentileComputer('ms for deserialize duration', valueLimit: 2000);
+}
+
+/// A client for communicating with the analysis server over stdin/stdout.
+class _AnalysisServerClient {
+ // This class is copied from package:dartdev/src/analysis_server.dart and
+ // stripped.
+
+ final Directory sdkPath;
+ final List<FileSystemEntity> analysisRoots;
+
+ Process? _process;
+
+ /// When not null, this is a [Completer] which completes when analysis has
+ /// finished, otherwise `null`.
+ Completer<bool>? _analysisFinished;
+
+ int _id = 0;
+
+ bool _shutdownResponseReceived = false;
+
+ final Map<String, StreamController<Map<String, dynamic>>> _streamControllers =
+ {};
+
+ final _onCrash = Completer<void>();
+
+ final Map<String, Completer<Map<String, dynamic>>> _requestCompleters = {};
+
+ final Map<String, _RequestMetadata> _requestMetadata = {};
+
+ _AnalysisServerClient(this.sdkPath, this.analysisRoots);
+
+ /// Completes when we next receive an analysis finished event (unless there's
+ /// no current analysis and we've already received a complete event, in which
+ /// case this future completes immediately).
+ Future<bool>? get analysisFinished => _analysisFinished?.future;
+
+ Stream<bool> get onAnalyzing {
+ // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
+ return _streamController('server.status')
+ .stream
+ .where((event) => event!['analysis'] != null)
+ .map((event) => event!['analysis']['isAnalyzing']! as bool);
+ }
+
+ /// Completes when an analysis server crash has been detected.
+ Future<void> get onCrash => _onCrash.future;
+
+ Future<int> get onExit => _process!.exitCode;
+
+ Future<bool> dispose() async {
+ return _process?.kill() ?? true;
+ }
+
+ /// Requests a completion for [file] at [offset].
+ Future<_SuggestionsData> requestCompletion(
+ String file, int offset, int maxResults) async {
+ final response = await _sendCommand('completion.getSuggestions2',
+ params: <String, dynamic>{
+ 'file': file,
+ 'offset': offset,
+ 'maxResults': maxResults,
+ });
+ final result = response['result'] as Map<String, dynamic>;
+ final metadata = _requestMetadata[response['id']]!;
+
+ final deserializeStopwatch = Stopwatch()..start();
+ final suggestionsResult = CompletionGetSuggestions2Result.fromJson(
+ ResponseDecoder(null),
+ 'result',
+ result,
+ );
+ deserializeStopwatch.stop();
+ metadata.deserializeDuration = deserializeStopwatch.elapsedMilliseconds;
+
+ return _SuggestionsData(suggestionsResult, metadata);
+ }
+
+ Future<void> shutdown({Duration timeout = const Duration(seconds: 5)}) async {
+ // Request shutdown.
+ await _sendCommand('server.shutdown').then((value) {
+ _shutdownResponseReceived = true;
+ return null;
+ }).timeout(timeout, onTimeout: () async {
+ logger.stderr('The analysis server timed out while shutting down.');
+ await dispose();
+ }).then((value) async {
+ await dispose();
+ });
+ }
+
+ Future<void> start({bool setAnalysisRoots = true}) async {
+ final process = await _startDartProcess(_sdk, [
+ _sdk.analysisServerSnapshot,
+ '--${Driver.SUPPRESS_ANALYTICS_FLAG}',
+ '--${Driver.CLIENT_ID}=completion-metrics-client',
+ '--sdk',
+ sdkPath.path,
+ ]);
+ _process = process;
+ _shutdownResponseReceived = false;
+ // This callback hookup can't throw.
+ process.exitCode.whenComplete(() {
+ _process = null;
+
+ if (!_shutdownResponseReceived) {
+ // The process exited unexpectedly. Report the crash.
+ // If `server.error` reported an error, that has been logged by
+ // `_handleServerError`.
+
+ final error = StateError('The analysis server crashed unexpectedly');
+
+ final analysisFinished = _analysisFinished;
+ if (analysisFinished != null && !analysisFinished.isCompleted) {
+ // Complete this completer in order to unstick the process.
+ analysisFinished.completeError(error);
+ }
+
+ // Complete these completers in order to unstick the process.
+ for (final completer in _requestCompleters.values) {
+ completer.completeError(error);
+ }
+
+ _onCrash.complete();
+ }
+ });
+
+ final errorStream = process.stderr
+ .transform<String>(utf8.decoder)
+ .transform<String>(const LineSplitter());
+ errorStream.listen(logger.stderr);
+
+ final inStream = process.stdout
+ .transform<String>(utf8.decoder)
+ .transform<String>(const LineSplitter());
+ inStream.listen(_handleServerResponse);
+
+ _streamController('server.error').stream.listen(_handleServerError);
+
+ _sendCommand('server.setSubscriptions', params: <String, dynamic>{
+ 'subscriptions': <String>['STATUS'],
+ });
+
+ // Reference and trim off any trailing slash, the Dart Analysis Server
+ // protocol throws an error (INVALID_FILE_PATH_FORMAT) if there is a
+ // trailing slash.
+ //
+ // The call to `absolute.resolveSymbolicLinksSync()` canonicalizes the path
+ // to be passed to the analysis server.
+ final analysisRootPaths = [
+ for (final root in analysisRoots)
+ _trimEnd(
+ root.absolute.resolveSymbolicLinksSync(), path.context.separator),
+ ];
+
+ onAnalyzing.listen((isAnalyzing) {
+ final analysisFinished = _analysisFinished;
+ if (isAnalyzing && (analysisFinished?.isCompleted ?? true)) {
+ // Start a new completer, to be completed when we receive the
+ // corresponding analysis complete event.
+ _analysisFinished = Completer();
+ } else if (!isAnalyzing &&
+ analysisFinished != null &&
+ !analysisFinished.isCompleted) {
+ analysisFinished.complete(true);
+ }
+ });
+
+ if (setAnalysisRoots) {
+ await _sendCommand('analysis.setAnalysisRoots', params: {
+ 'included': analysisRootPaths,
+ 'excluded': [],
+ });
+ }
+ }
+
+ void _handleServerError(Map<String, dynamic>? error) {
+ final err = error!;
+ // Fields are 'isFatal', 'message', and 'stackTrace'.
+ logger.stderr('Error from the analysis server: ${err['message']}');
+ if (err['stackTrace'] != null) {
+ logger.stderr(err['stackTrace'] as String);
+ }
+ }
+
+ void _handleServerResponse(String line) {
+ logger.trace('<== $line');
+
+ var responseTime = DateTime.now().millisecondsSinceEpoch;
+
+ final decodeStopwatch = Stopwatch()..start();
+ final dynamic response = json.decode(line);
+ decodeStopwatch.stop();
+ var decodeDuration = decodeStopwatch.elapsedMilliseconds;
+
+ if (response is Map<String, dynamic>) {
+ if (response['event'] != null) {
+ final event = response['event'] as String;
+ final dynamic params = response['params'];
+
+ if (params is Map<String, dynamic>) {
+ _streamController(event).add(_castStringKeyedMap(params));
+ }
+ } else if (response['id'] != null) {
+ final id = response['id'];
+ final metadata = _requestMetadata[id]!;
+ metadata.responseMilliseconds = responseTime;
+ metadata.decodeDuration = decodeDuration;
+
+ if (response['error'] != null) {
+ final error = _castStringKeyedMap(response['error']);
+ _requestCompleters
+ .remove(id)
+ ?.completeError(_RequestError.parse(error));
+ } else {
+ _requestCompleters.remove(id)?.complete(
+ //response['result'] as Map<String, dynamic>? ??
+ // <String, dynamic>{});
+ response);
+ }
+ }
+ }
+ }
+
+ Future<Map<String, dynamic>> _sendCommand(String method,
+ {Map<String, dynamic>? params}) {
+ final String id = (++_id).toString();
+ final String message = json.encode({
+ 'id': id,
+ 'method': method,
+ 'params': params,
+ });
+ _requestMetadata[id] =
+ _RequestMetadata(DateTime.now().millisecondsSinceEpoch);
+ _requestCompleters[id] = Completer();
+ _process!.stdin.writeln(message);
+ logger.trace('==> $message');
+ return _requestCompleters[id]!.future;
+ }
+
+ /// A utility method to start a Dart VM instance with the given arguments and an
+ /// optional current working directory.
+ ///
+ /// [arguments] should contain the snapshot path.
+ Future<Process> _startDartProcess(
+ _Sdk sdk,
+ List<String> arguments, {
+ String? cwd,
+ }) {
+ logger.trace('${sdk.dart} ${arguments.join(' ')}');
+ return Process.start(sdk.dart, arguments, workingDirectory: cwd);
+ }
+
+ StreamController<Map<String, dynamic>?> _streamController(String streamId) {
+ return _streamControllers.putIfAbsent(
+ streamId, () => StreamController<Map<String, dynamic>>.broadcast());
+ }
+}
+
+class _RequestError {
+ // This is copied from package:dartdev/src/analysis_server.dart.
+
+ final String code;
+
+ final String message;
+ final String stackTrace;
+ _RequestError(this.code, this.message, {required this.stackTrace});
+
+ @override
+ String toString() => '[RequestError code: $code, message: $message]';
+
+ static _RequestError parse(dynamic error) {
+ return _RequestError(
+ error['code'] as String,
+ error['message'] as String,
+ stackTrace: error['stackTrace'] as String,
+ );
+ }
+}
+
+class _RequestMetadata {
+ /// The timestamp of when a request was started, in milliseconds.
+ ///
+ /// This does not include the time it takes to encode the request into JSON.
+ final int startMilliseconds;
+
+ /// The timestamp of when a response was received, in milliseconds.
+ late final int responseMilliseconds;
+
+ /// The duration of decoding a response, in milliseconds.
+ late final int decodeDuration;
+
+ /// The duration of deserializing a response, in milliseconds.
+ late final int deserializeDuration;
+
+ _RequestMetadata(this.startMilliseconds);
+
+ /// The duration of time between sending a completion request and receiving a
+ /// completion response, not including the time to decode the response.
+ int get requestResponseDuration => responseMilliseconds - startMilliseconds;
+}
+
+/// A utility class for finding and referencing paths within the Dart SDK.
+class _Sdk {
+ // This is copied from package:dartdev/src/sdk.dart and stripped.
+
+ static final _Sdk _instance = _createSingleton();
+
+ /// Path to SDK directory.
+ final String sdkPath;
+
+ factory _Sdk() => _instance;
+
+ _Sdk._(this.sdkPath);
+
+ String get analysisServerSnapshot => path.absolute(
+ sdkPath,
+ 'bin',
+ 'snapshots',
+ 'analysis_server.dart.snapshot',
+ );
+
+ // Assume that we want to use the same Dart executable that we used to spawn
+ // DartDev. We should be able to run programs with out/ReleaseX64/dart even
+ // if the SDK isn't completely built.
+ String get dart => Platform.resolvedExecutable;
+
+ static _Sdk _createSingleton() {
+ // Find SDK path.
+
+ // The common case, and how cli_util.dart computes the Dart SDK directory,
+ // [path.dirname] called twice on Platform.resolvedExecutable. We confirm by
+ // asserting that the directory `./bin/snapshots/` exists in this directory:
+ var sdkPath =
+ path.absolute(path.dirname(path.dirname(Platform.resolvedExecutable)));
+ var snapshotsDir = path.join(sdkPath, 'bin', 'snapshots');
+ if (!Directory(snapshotsDir).existsSync()) {
+ // This is the less common case where the user is in
+ // the checked out Dart SDK, and is executing `dart` via:
+ // ./out/ReleaseX64/dart ...
+ // We confirm in a similar manner with the snapshot directory existence
+ // and then return the correct sdk path:
+ var altPath =
+ path.absolute(path.dirname(Platform.resolvedExecutable), 'dart-sdk');
+ var snapshotsDir = path.join(altPath, 'bin', 'snapshots');
+ if (Directory(snapshotsDir).existsSync()) {
+ sdkPath = altPath;
+ }
+ // If that snapshot dir does not exist either,
+ // we use the first guess anyway.
+ }
+
+ return _Sdk._(sdkPath);
+ }
+}
+
+/// A container which pairs a [CompletionGetSuggestions2Result] with the
+/// [_RequestMetadata] which is associated with the result's completion request
+/// and response.
+class _SuggestionsData {
+ final CompletionGetSuggestions2Result result;
+ final _RequestMetadata metadata;
+
+ _SuggestionsData(this.result, this.metadata);
+}
diff --git a/tools/VERSION b/tools/VERSION
index a5e92bf..bb151a6 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 18
PATCH 0
-PRERELEASE 83
+PRERELEASE 84
PRERELEASE_PATCH 0
\ No newline at end of file