Index: pkg/analysis_server/lib/src/plugin/plugin_manager.dart |
diff --git a/pkg/analysis_server/lib/src/plugin/plugin_manager.dart b/pkg/analysis_server/lib/src/plugin/plugin_manager.dart |
index 01e26286effe3480b6c17c5250826d95a0e991a9..b321d30d9756c79798601f788bddacc48e464885 100644 |
--- a/pkg/analysis_server/lib/src/plugin/plugin_manager.dart |
+++ b/pkg/analysis_server/lib/src/plugin/plugin_manager.dart |
@@ -235,6 +235,14 @@ abstract class PluginInfo { |
*/ |
class PluginManager { |
/** |
+ * A table, keyed by both a plugin and a request method, to a list of the |
+ * times that it took the plugin to return a response to requests with the |
+ * method. |
+ */ |
+ static Map<PluginInfo, Map<String, List<int>>> pluginResponseTimes = |
+ <PluginInfo, Map<String, List<int>>>{}; |
+ |
+ /** |
* The resource provider used to access the file system. |
*/ |
final ResourceProvider resourceProvider; |
@@ -567,6 +575,17 @@ class PluginManager { |
List<int> bytes = md5.convert(path.codeUnits).bytes; |
return hex.encode(bytes); |
} |
+ |
+ /** |
+ * Record the fact that the given [plugin] responded to a request with the |
+ * given [method] in the given [time]. |
+ */ |
+ static void recordResponseTime(PluginInfo plugin, String method, int time) { |
+ pluginResponseTimes |
+ .putIfAbsent(plugin, () => <String, List<int>>{}) |
+ .putIfAbsent(method, () => <int>[]) |
+ .add(time); |
+ } |
} |
/** |
@@ -575,6 +594,19 @@ class PluginManager { |
@visibleForTesting |
class PluginSession { |
/** |
+ * The maximum number of milliseconds that server should wait for a response |
+ * from a plugin before deciding that the plugin is hung. |
+ */ |
+ static const Duration MAXIMUM_RESPONSE_TIME = const Duration(minutes: 2); |
+ |
+ /** |
+ * The length of time to wait after sending a 'plugin.shutdown' request before |
+ * a failure to terminate will cause the isolate to be killed. |
+ */ |
+ static const Duration WAIT_FOR_SHUTDOWN_DURATION = |
+ const Duration(seconds: 10); |
+ |
+ /** |
* The information about the plugin being executed. |
*/ |
final PluginInfo info; |
@@ -598,8 +630,7 @@ class PluginSession { |
* A table mapping the id's of requests to the functions used to handle the |
* response to those requests. |
*/ |
- Map<String, Completer<Response>> pendingRequests = |
- <String, Completer<Response>>{}; |
+ Map<String, _PendingRequest> pendingRequests = <String, _PendingRequest>{}; |
/** |
* A boolean indicating whether the plugin is compatible with the version of |
@@ -689,13 +720,34 @@ class PluginSession { |
* created when the request was sent. |
*/ |
void handleResponse(Response response) { |
- Completer<Response> completer = pendingRequests.remove(response.id); |
+ _PendingRequest requestData = pendingRequests.remove(response.id); |
+ int responseTime = new DateTime.now().millisecondsSinceEpoch; |
+ int duration = responseTime - requestData.requestTime; |
+ PluginManager.recordResponseTime(info, requestData.method, duration); |
+ Completer<Response> completer = requestData.completer; |
if (completer != null) { |
completer.complete(response); |
} |
} |
/** |
+ * Return `true` if there are any requests that have not been responded to |
+ * within the maximum allowed amount of time. |
+ */ |
+ bool isNonResponsive() { |
+ // TODO(brianwilkerson) Figure out when to invoke this method in order to |
+ // identify non-responsive plugins and kill them. |
+ int cutOffTime = new DateTime.now().millisecondsSinceEpoch - |
+ MAXIMUM_RESPONSE_TIME.inMilliseconds; |
+ for (var requestData in pendingRequests.values) { |
+ if (requestData.requestTime < cutOffTime) { |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ /** |
* Send a request, based on the given [parameters]. Return a future that will |
* complete when a response is received. |
*/ |
@@ -706,8 +758,11 @@ class PluginSession { |
} |
String id = nextRequestId; |
Completer<Response> completer = new Completer(); |
- pendingRequests[id] = completer; |
- channel.sendRequest(parameters.toRequest(id)); |
+ int requestTime = new DateTime.now().millisecondsSinceEpoch; |
+ Request request = parameters.toRequest(id); |
+ pendingRequests[id] = |
+ new _PendingRequest(request.method, requestTime, completer); |
+ channel.sendRequest(request); |
return completer.future; |
} |
@@ -757,9 +812,40 @@ class PluginSession { |
if (channel == null) { |
throw new StateError('Cannot stop a plugin that is not running.'); |
} |
- // TODO(brianwilkerson) Ensure that the isolate is killed if it does not |
- // terminate normally. |
sendRequest(new PluginShutdownParams()); |
+ new Future.delayed(WAIT_FOR_SHUTDOWN_DURATION, () { |
+ if (channel != null) { |
+ channel.kill(); |
+ channel = null; |
+ } |
+ }); |
return pluginStoppedCompleter.future; |
} |
} |
+ |
+/** |
+ * Information about a request that has been sent but for which a response has |
+ * not yet been received. |
+ */ |
+class _PendingRequest { |
+ /** |
+ * The method of the request. |
+ */ |
+ final String method; |
+ |
+ /** |
+ * The time at which the request was sent to the plugin. |
+ */ |
+ final int requestTime; |
scheglov
2017/05/18 21:36:16
Or you could use new Stopwatch()..start() instead.
Brian Wilkerson
2017/05/18 21:51:39
I hadn't considered that, but now that I think abo
scheglov
2017/05/18 21:55:55
It's fine with my any way, just thought about othe
|
+ |
+ /** |
+ * The completer that will be used to complete the future when the response is |
+ * received from the plugin. |
+ */ |
+ final Completer<Response> completer; |
+ |
+ /** |
+ * Initialize a pending request. |
+ */ |
+ _PendingRequest(this.method, this.requestTime, this.completer); |
+} |