Cache documentation comments to improve completion performance

I'm not convinced that this is the right way to cache comments, so
feedback is welcome.

Change-Id: I2bd393b80e05c26199020e19daf0eadeb99d8c2b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/199303
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Brian Wilkerson 2021-05-13 16:31:35 +00:00 committed by commit-bot@chromium.org
parent 04fb485fd5
commit b42c5c77d4
8 changed files with 359 additions and 110 deletions

View file

@ -685,6 +685,7 @@ class ServerContextManagerCallbacks extends ContextManagerCallbacks {
_notificationManager.recordAnalysisErrors(NotificationManager.serverId,
path, server.doAnalysisError_listFromEngine(result));
}
analysisServer.getDocumentationCacheFor(result)?.cacheFromResult(result);
var unit = result.unit;
if (unit != null) {
if (analysisServer._hasAnalysisServiceSubscription(

View file

@ -15,6 +15,7 @@ import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/plugin/plugin_watcher.dart';
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
import 'package:analysis_server/src/services/correction/namespace.dart';
import 'package:analysis_server/src/services/pub/pub_api.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
@ -25,6 +26,7 @@ import 'package:analysis_server/src/utilities/file_string_sink.dart';
import 'package:analysis_server/src/utilities/null_string_sink.dart';
import 'package:analysis_server/src/utilities/request_statistics.dart';
import 'package:analysis_server/src/utilities/tee_string_sink.dart';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
@ -83,6 +85,10 @@ abstract class AbstractAnalysisServer {
DeclarationsTracker? declarationsTracker;
DeclarationsTrackerData? declarationsTrackerData;
/// A map from analysis contexts to the documentation cache associated with
/// each context.
Map<AnalysisContext, DocumentationCache> documentationForContext = {};
/// The DiagnosticServer for this AnalysisServer. If available, it can be used
/// to start an http diagnostics server or return the port for an existing
/// server.
@ -217,6 +223,7 @@ abstract class AbstractAnalysisServer {
void addContextsToDeclarationsTracker() {
declarationsTracker?.discardContexts();
documentationForContext.clear();
for (var driver in driverMap.values) {
declarationsTracker?.addContext(driver.analysisContext!);
driver.resetUriResolution();
@ -275,6 +282,19 @@ abstract class AbstractAnalysisServer {
DartdocDirectiveInfo();
}
/// Return the object used to cache the documentation for elements in the
/// context that produced the [result], or `null` if there is no cache for the
/// context.
DocumentationCache? getDocumentationCacheFor(ResolvedUnitResult result) {
var context = result.session.analysisContext;
var tracker = declarationsTracker?.getContext(context);
if (tracker == null) {
return null;
}
return documentationForContext.putIfAbsent(
context, () => DocumentationCache(tracker.dartdocDirectiveInfo));
}
/// Return a [Future] that completes with the [Element] at the given
/// [offset] of the given [file], or with `null` if there is no node at the
/// [offset] or the node does not have an element.

View file

@ -108,7 +108,9 @@ class CompletionDomainHandler extends AbstractRequestHandler {
await perf.runAsync(contributorTag, (performance) async {
try {
suggestions.addAll(
await manager.computeSuggestions(performance, request),
await manager.computeSuggestions(performance, request,
documentationCache:
server.getDocumentationCacheFor(request.result)),
);
} on AbortCompletion {
suggestions.clear();

View file

@ -9,6 +9,7 @@ import 'package:analysis_server/src/services/completion/completion_core.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/arglist_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/combinator_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
import 'package:analysis_server/src/services/completion/dart/extension_member_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
import 'package:analysis_server/src/services/completion/dart/field_formal_contributor.dart';
@ -93,6 +94,7 @@ class DartCompletionManager {
bool enableOverrideContributor = true,
bool enableUriContributor = true,
CompletionPreference? completionPreference,
DocumentationCache? documentationCache,
}) async {
request.checkAborted();
var pathContext = request.resourceProvider.pathContext;
@ -105,6 +107,7 @@ class DartCompletionManager {
request,
dartdocDirectiveInfo,
completionPreference: completionPreference,
documentationCache: documentationCache,
);
// Don't suggest in comments.
@ -297,6 +300,8 @@ class DartCompletionRequestImpl implements DartCompletionRequest {
@override
final CompletionPreference completionPreference;
final DocumentationCache? documentationCache;
DartCompletionRequestImpl._(
this.result,
this.resourceProvider,
@ -308,7 +313,8 @@ class DartCompletionRequestImpl implements DartCompletionRequest {
this.dartdocDirectiveInfo,
this._originalRequest,
this.performance,
{CompletionPreference? completionPreference})
{CompletionPreference? completionPreference,
this.documentationCache})
: featureComputer =
FeatureComputer(result.typeSystem, result.typeProvider),
completionPreference =
@ -431,7 +437,8 @@ class DartCompletionRequestImpl implements DartCompletionRequest {
OperationPerformanceImpl performance,
CompletionRequest request,
DartdocDirectiveInfo? dartdocDirectiveInfo,
{CompletionPreference? completionPreference}) async {
{CompletionPreference? completionPreference,
DocumentationCache? documentationCache}) async {
request.checkAborted();
return performance.run(
@ -453,6 +460,7 @@ class DartCompletionRequestImpl implements DartCompletionRequest {
request,
(request as CompletionRequestImpl).performance,
completionPreference: completionPreference,
documentationCache: documentationCache,
);
},
);

View file

@ -0,0 +1,196 @@
// 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 'package:analysis_server/src/computer/computer_hover.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
/// Cached data about the documentation associated with the elements declared in
/// a single analysis context.
class DocumentationCache {
/// A shared instance for elements that have no documentation.
static final DocumentationWithSummary _emptyDocs =
DocumentationWithSummary(full: '', summary: '');
/// The object used to compute the documentation associated with a single
/// element.
final DartdocDirectiveInfo dartdocDirectiveInfo;
/// The documentation associated with the elements that have been cached. The
/// cache is keyed by the path of the file containing the declaration of the
/// element and the qualified name of the element.
final Map<String, Map<String, DocumentationWithSummary>> documentationCache =
{};
/// Initialize a newly created cache.
DocumentationCache(this.dartdocDirectiveInfo);
/// Fill the cache with data from the [result].
void cacheFromResult(ResolvedUnitResult result) {
var element = result.unit?.declaredElement;
if (element != null) {
_cacheFromElement(element);
for (var library in result.libraryElement.importedLibraries) {
_cacheLibrary(library);
}
}
}
/// Return the data cached for the given [element], or `null` if there is no
/// cached data.
DocumentationWithSummary? dataFor(Element element) {
var parent = element.enclosingElement;
if (parent == null) {
return null;
}
var key = element.name;
if (key == null) {
return null;
}
if (parent is! CompilationUnitElement) {
var parentName = parent.name;
if (parentName == null) {
return null;
}
key = '$parentName.$key';
parent = parent.enclosingElement;
}
if (parent is CompilationUnitElement) {
var elementMap = documentationCache[_keyForUnit(parent)];
return elementMap?[key];
}
return null;
}
/// Fill the cache with data from the [compilationUnit].
void _cacheFromElement(CompilationUnitElement compilationUnit) {
var elementMap =
documentationCache.putIfAbsent(_keyForUnit(compilationUnit), () => {});
for (var element in compilationUnit.accessors) {
if (!element.isSynthetic) {
elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element);
}
}
for (var element in compilationUnit.enums) {
var parentKey =
elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element);
if (parentKey != null) {
for (var member in element.fields) {
elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member);
}
}
}
for (var element in compilationUnit.extensions) {
var parentKey =
elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element);
if (parentKey != null) {
for (var member in element.accessors) {
if (!element.isSynthetic) {
elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member);
}
}
for (var member in element.fields) {
if (!element.isSynthetic) {
elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member);
}
}
for (var member in element.methods) {
elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member);
}
}
}
for (var element in compilationUnit.functions) {
elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element);
}
for (var element in [...compilationUnit.mixins, ...compilationUnit.types]) {
var parentKey =
elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element);
if (parentKey != null) {
for (var member in element.accessors) {
if (!element.isSynthetic) {
elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member);
}
}
for (var member in element.fields) {
if (!element.isSynthetic) {
elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member);
}
}
for (var member in element.methods) {
elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member);
}
}
}
for (var element in compilationUnit.topLevelVariables) {
if (!element.isSynthetic) {
elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element);
}
}
for (var element in compilationUnit.typeAliases) {
elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element);
}
}
/// Cache the data for the given [library] and every library exported from it
/// if it hasn't already been cached.
void _cacheLibrary(LibraryElement library) {
if (_hasDataFor(library.definingCompilationUnit)) {
return;
}
for (var unit in library.units) {
_cacheFromElement(unit);
}
for (var exported in library.exportedLibraries) {
_cacheLibrary(exported);
}
}
/// Return `true` if the cache contains data for the [compilationUnit].
bool _hasDataFor(CompilationUnitElement compilationUnit) {
return documentationCache.containsKey(_keyForUnit(compilationUnit));
}
/// Return the key used in the [documentationCache] for the [compilationUnit].
String _keyForUnit(CompilationUnitElement compilationUnit) =>
compilationUnit.source.fullName;
}
extension on Map<String, DocumentationWithSummary> {
/// Cache the data associated with the top-level [element], and return the
/// [key] used for the element. This does not cache any data associated with
/// any other elements, including children of the [element].
String? cacheTopLevelElement(
DartdocDirectiveInfo dartdocDirectiveInfo, Element element) {
var key = element.name;
if (key == null) {
return null;
}
cacheElement(dartdocDirectiveInfo, key, element);
return key;
}
/// Cache the data associated with the [member] element given that the key
/// associated with the member's parent is [parentKey].
void cacheMember(DartdocDirectiveInfo dartdocDirectiveInfo, String parentKey,
Element member) {
var name = member.name;
if (name == null) {
return null;
}
cacheElement(dartdocDirectiveInfo, '$parentKey.$name', member);
}
/// Cache the data associated with the [element], using the given [key].
DocumentationWithSummary? cacheElement(
DartdocDirectiveInfo dartdocDirectiveInfo, String key, Element element) {
var documentation = DartUnitHoverComputer.computeDocumentation(
dartdocDirectiveInfo, element,
includeSummary: true);
if (documentation is DocumentationWithSummary) {
return this[key] = documentation;
}
return this[key] = DocumentationCache._emptyDocs;
}
}

View file

@ -10,6 +10,7 @@ import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/protocol_server.dart'
hide Element, ElementKind;
import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
import 'package:analysis_server/src/services/completion/dart/utilities.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
@ -1110,6 +1111,16 @@ class SuggestionBuilder {
/// If the [element] has a documentation comment, fill the [suggestion]'s
/// documentation fields.
void _setDocumentation(CompletionSuggestion suggestion, Element element) {
final request = this.request;
if (request is DartCompletionRequestImpl) {
var documentationCache = request.documentationCache;
var data = documentationCache?.dataFor(element);
if (data != null) {
suggestion.docComplete = data.full;
suggestion.docSummary = data.summary;
return;
}
}
var doc = DartUnitHoverComputer.computeDocumentation(
request.dartdocDirectiveInfo, element,
includeSummary: true);

View file

@ -13,6 +13,7 @@ import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/services/completion/completion_core.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
import 'package:analysis_server/src/services/completion/dart/probability_range.dart';
import 'package:analysis_server/src/services/completion/dart/relevance_tables.g.dart';
@ -1034,6 +1035,8 @@ class CompletionMetricsComputer {
MetricsSuggestionListener listener,
OperationPerformanceImpl performance,
CompletionRequestImpl request,
DartdocDirectiveInfo dartdocDirectiveInfo,
DocumentationCache? documentationCache,
[DeclarationsTracker? declarationsTracker,
protocol.CompletionAvailableSuggestionsParams?
availableSuggestionsParams]) async {
@ -1042,9 +1045,10 @@ class CompletionMetricsComputer {
if (declarationsTracker == null) {
// available suggestions == false
suggestions = await DartCompletionManager(
dartdocDirectiveInfo: DartdocDirectiveInfo(),
dartdocDirectiveInfo: dartdocDirectiveInfo,
listener: listener,
).computeSuggestions(performance, request);
).computeSuggestions(performance, request,
documentationCache: documentationCache);
} else {
// available suggestions == true
var includedElementKinds = <protocol.ElementKind>{};
@ -1053,12 +1057,13 @@ class CompletionMetricsComputer {
<protocol.IncludedSuggestionRelevanceTag>[];
var includedSuggestionSetList = <protocol.IncludedSuggestionSet>[];
suggestions = await DartCompletionManager(
dartdocDirectiveInfo: DartdocDirectiveInfo(),
dartdocDirectiveInfo: dartdocDirectiveInfo,
includedElementKinds: includedElementKinds,
includedElementNames: includedElementNames,
includedSuggestionRelevanceTags: includedSuggestionRelevanceTagList,
listener: listener,
).computeSuggestions(performance, request);
).computeSuggestions(performance, request,
documentationCache: documentationCache);
computeIncludedSetList(declarationsTracker, request.result,
includedSuggestionSetList, includedElementNames);
@ -1141,118 +1146,26 @@ class CompletionMetricsComputer {
// Loop through each file, resolve the file and call
// forEachExpectedCompletion
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 {
_resolvedUnitResult = await context.currentSession
.getResolvedUnit2(filePath) as ResolvedUnitResult;
var result = await context.currentSession.getResolvedUnit2(filePath)
as ResolvedUnitResult;
var analysisError = getFirstErrorOrNull(_resolvedUnitResult);
var analysisError = getFirstErrorOrNull(result);
if (analysisError != null) {
print('File $filePath skipped due to errors such as:');
print(' ${analysisError.toString()}');
print('');
continue;
}
// Use the ExpectedCompletionsVisitor to compute the set of expected
// completions for this CompilationUnit.
final visitor = ExpectedCompletionsVisitor(filePath);
_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.driver.changeFile(filePath);
resolvedUnitResult = await context.currentSession
.getResolvedUnit2(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 = CompletionRequestImpl(
resolvedUnitResult,
expectedCompletion.offset,
CompletionPerformance(),
);
var directiveInfo = DartdocDirectiveInfo();
late OpType opType;
late List<protocol.CompletionSuggestion> suggestions;
await request.performance.runRequestOperation(
(performance) async {
var dartRequest = await DartCompletionRequestImpl.from(
performance, request, directiveInfo);
opType =
OpType.forCompletion(dartRequest.target, request.offset);
suggestions = await _computeCompletionSuggestions(
listener,
performance,
request,
metrics.availableSuggestions ? declarationsTracker : null,
metrics.availableSuggestions
? availableSuggestionsParams
: null,
);
},
);
stopwatch.stop();
return forEachExpectedCompletion(
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();
// if (FeatureComputer.noDisabledFeatures) {
// var line = expectedCompletion.lineNumber;
// var column = expectedCompletion.columnNumber;
// print('$filePath:$line:$column');
// }
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);
}
} else {
results.add(result);
documentationCache.cacheFromResult(result);
}
} catch (exception, stackTrace) {
print('Exception caught analyzing: $filePath');
@ -1261,6 +1174,104 @@ class CompletionMetricsComputer {
}
}
}
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(filePath);
_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.driver.changeFile(filePath);
resolvedUnitResult = await context.currentSession
.getResolvedUnit2(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 = CompletionRequestImpl(
resolvedUnitResult,
expectedCompletion.offset,
CompletionPerformance(),
);
var directiveInfo = DartdocDirectiveInfo();
late OpType opType;
late List<protocol.CompletionSuggestion> suggestions;
await request.performance.runRequestOperation(
(performance) async {
var dartRequest = await DartCompletionRequestImpl.from(
performance, request, directiveInfo);
opType = OpType.forCompletion(dartRequest.target, request.offset);
suggestions = await _computeCompletionSuggestions(
listener,
performance,
request,
dartdocDirectiveInfo,
documentationCache,
metrics.availableSuggestions ? declarationsTracker : null,
metrics.availableSuggestions
? availableSuggestionsParams
: null,
);
},
);
stopwatch.stop();
return forEachExpectedCompletion(
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);
}
}
}
}
List<protocol.CompletionSuggestion> _filterSuggestions(

View file

@ -177,7 +177,7 @@ class DartdocDirectiveInfo {
}
start = eolIndex + 1;
}
if (lastNonEmpty < firstNonEmpty) {
if (firstNonEmpty < 0 || lastNonEmpty < firstNonEmpty) {
// All of the lines are empty.
return const <String>[];
}