| // Copyright (c) 2015, 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. |
| |
| library src.domain_diagnostic; |
| |
| import 'dart:async'; |
| import 'dart:collection'; |
| import 'dart:core' hide Resource; |
| |
| import 'package:analysis_server/plugin/protocol/protocol.dart'; |
| import 'package:analysis_server/src/analysis_server.dart'; |
| import 'package:analysis_server/src/utilities/average.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/src/context/cache.dart'; |
| import 'package:analyzer/src/context/context.dart'; |
| import 'package:analyzer/src/generated/engine.dart' |
| hide AnalysisCache, AnalysisContextImpl; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/generated/utilities_collection.dart'; |
| import 'package:analyzer/src/task/driver.dart'; |
| import 'package:analyzer/task/model.dart'; |
| |
| int _workItemCount(AnalysisContextImpl context) { |
| AnalysisDriver driver = context.driver; |
| List<WorkItem> items = driver.currentWorkOrder?.workItems; |
| return items?.length ?? 0; |
| } |
| |
| /// Instances of the class [DiagnosticDomainHandler] implement a |
| /// [RequestHandler] that handles requests in the `diagnostic` domain. |
| class DiagnosticDomainHandler implements RequestHandler { |
| /// The name of the request used to get diagnostic information. |
| static const String DIAGNOSTICS = 'diagnostic.getDiagnostics'; |
| |
| /// The analysis server that is using this handler to process requests. |
| final AnalysisServer server; |
| |
| /// The sampler tracking rolling work queue length averages. |
| Sampler sampler; |
| |
| /// Initialize a newly created handler to handle requests for the given |
| /// [server]. |
| DiagnosticDomainHandler(this.server); |
| |
| /// Answer the `diagnostic.diagnostics` request. |
| Response computeDiagnostics(Request request) { |
| // Initialize sampler if needed. |
| if (sampler == null) { |
| sampler = new Sampler(server); |
| } |
| |
| List<ContextData> infos = <ContextData>[]; |
| server.folderMap.forEach((Folder folder, AnalysisContext context) { |
| infos.add(extractData(folder, context)); |
| }); |
| |
| return new DiagnosticGetDiagnosticsResult(infos).toResponse(request.id); |
| } |
| |
| /// Extract context data from the given [context]. |
| ContextData extractData(Folder folder, AnalysisContext context) { |
| int explicitFiles = 0; |
| int implicitFiles = 0; |
| int workItems = 0; |
| String workItemAverage = '-1'; |
| Set<String> exceptions = new HashSet<String>(); |
| if (context is AnalysisContextImpl) { |
| workItems = _workItemCount(context); |
| workItemAverage = sampler.getAverage(folder)?.toString() ?? '-1'; |
| var cache = context.analysisCache; |
| if (cache is AnalysisCache) { |
| Set<AnalysisTarget> countedTargets = new HashSet<AnalysisTarget>(); |
| MapIterator<AnalysisTarget, CacheEntry> iterator = cache.iterator(); |
| while (iterator.moveNext()) { |
| AnalysisTarget target = iterator.key; |
| if (countedTargets.add(target)) { |
| CacheEntry cacheEntry = iterator.value; |
| if (target is Source) { |
| if (cacheEntry.explicitlyAdded) { |
| explicitFiles++; |
| } else { |
| implicitFiles++; |
| } |
| } |
| // Caught exceptions. |
| if (cacheEntry.exception != null) { |
| exceptions.add(cacheEntry.exception.toString()); |
| } |
| } |
| } |
| } |
| } |
| return new ContextData(context.name, explicitFiles, implicitFiles, |
| workItems, workItemAverage, exceptions.toList()); |
| } |
| |
| @override |
| Response handleRequest(Request request) { |
| try { |
| String requestName = request.method; |
| if (requestName == DIAGNOSTICS) { |
| return computeDiagnostics(request); |
| } |
| } on RequestFailure catch (exception) { |
| return exception.response; |
| } |
| return null; |
| } |
| } |
| |
| /// Keeps track of a moving average of work item queue lengths mapped to |
| /// contexts. |
| /// |
| /// Sampling terminates after [maxSampleCount], if no one expresses interest |
| /// by calling [resetTimerCountdown]. |
| class Sampler { |
| /// Timer interval. |
| static const Duration duration = const Duration(seconds: 1); |
| |
| /// Maximum number of samples taken between calls to [reset]. |
| static const int maxSampleCount = 30; |
| |
| /// Current sample count. |
| int sampleCount = 0; |
| |
| /// The shared timer. |
| Timer timer; |
| |
| /// Map of contexts (tracked as folders to avoid leaks) to averages. |
| /// TOOD(pq): consider adding GC to remove mappings for deleted folders |
| Map<Folder, Average> averages = new HashMap<Folder, Average>(); |
| |
| final AnalysisServer server; |
| Sampler(this.server) { |
| start(); |
| _sample(); |
| } |
| |
| /// Get the average for the context associated with the given [folder]. |
| num getAverage(Folder folder) { |
| resetTimerCountdown(); |
| return averages[folder].value; |
| } |
| |
| /// Check if we're currently sampling. |
| bool isSampling() => timer?.isActive ?? false; |
| |
| /// Reset counter. |
| void resetTimerCountdown() { |
| sampleCount = 0; |
| } |
| |
| /// Start sampling. |
| void start() { |
| // No need to (re)start if already sampling. |
| if (isSampling()) { |
| return; |
| } |
| timer = new Timer.periodic(duration, (Timer timer) { |
| _sample(); |
| if (sampleCount++ >= maxSampleCount) { |
| timer.cancel(); |
| } |
| }); |
| } |
| |
| /// Stop sampling. |
| void stop() { |
| timer.cancel(); |
| } |
| |
| /// Take a sample. |
| void _sample() { |
| try { |
| server.folderMap.forEach((Folder folder, AnalysisContext context) { |
| if (context is AnalysisContextImpl) { |
| Average average = averages[folder]; |
| if (average == null) { |
| average = new Average(); |
| averages[folder] = average; |
| } |
| average.addSample(_workItemCount(context)); |
| } |
| }); |
| } on Exception { |
| stop(); |
| } |
| } |
| } |