| // Copyright (c) 2017, 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 'package:analysis_server/src/analysis_server.dart'; |
| import 'package:analysis_server/src/plugin/plugin_manager.dart'; |
| import 'package:analysis_server/src/status/diagnostics.dart'; |
| import 'package:analysis_server/src/status/utilities/string_extensions.dart'; |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; |
| import 'package:path/path.dart' as path; |
| |
| /// The page that displays information about _new_ (not legacy) analyzer |
| /// plugins. |
| class PluginsPage extends DiagnosticPageWithNav { |
| @override |
| AnalysisServer server; |
| |
| new(DiagnosticsSite site, this.server) |
| : super(site, 'plugins', 'Plugins', description: 'Plugins in use.'); |
| |
| @override |
| Future<void> generateContent(Map<String, String> params) async { |
| var pluginIsolates = server.pluginManager.newPluginIsolates; |
| |
| if (pluginIsolates.isEmpty) { |
| blankslate('No known analysis plugins.'); |
| return; |
| } |
| |
| pluginIsolates.sort( |
| (first, second) => first.pluginId.compareTo(second.pluginId), |
| ); |
| for (var isolate in pluginIsolates) { |
| var id = isolate.pluginId; |
| var data = isolate.data; |
| |
| p( |
| 'The following plugins are running from a single bootstrapped ' |
| 'location:', |
| ); |
| |
| _emitTable([ |
| ['Bootstrap package path:', id], |
| if (isolate.executionPath case var executionPath?) |
| ['Execution path:', executionPath.wordBreakOnSlashes], |
| if (isolate.packageConfigPath case var packageConfigPath?) |
| [ |
| 'Package config path:', |
| '${path.posix.dirname(packageConfigPath)}/'.wordBreakOnSlashes + |
| formatContentsLink( |
| packageConfigPath, |
| file_paths.packageConfigJson, |
| ), |
| ], |
| ]); |
| |
| if (data.name == null) { |
| if (isolate.exception != null) { |
| p('Not running due to:'); |
| pre(() { |
| buf.write(isolate.exception); |
| }); |
| } else { |
| p( |
| 'Not running for unknown reason (no exception was caught while ' |
| 'starting).', |
| ); |
| } |
| continue; |
| } |
| |
| h3('Associated contexts:'); |
| var contexts = isolate.contextRoots; |
| if (contexts.isEmpty) { |
| blankslate('none'); |
| } else { |
| ul(contexts.toList(), (root) { |
| buf.writeln(root.root); |
| }); |
| } |
| |
| var details = await isolate.requestDetails(); |
| if (details == null) { |
| // Either the plugin is not alive, or the plugin did not respond in |
| // time. |
| // TODO(srawlins): Distinguish between the two. |
| p('Plugin isolate did not respond with plugin details.'); |
| } else { |
| for (var plugin in details.plugins) { |
| h3(plugin.name); |
| |
| var prints = isolate.pluginPrints[plugin.name]; |
| if (prints != null) { |
| h3('Debug print output:'); |
| pre(() { |
| var discardedPrintCount = |
| isolate.discardedPluginPrintCount[plugin.name]; |
| if (discardedPrintCount != null && discardedPrintCount > 0) { |
| buf.write('(... $discardedPrintCount previous messages)'); |
| } |
| for (var print in prints) { |
| var timestamp = DateTime.fromMillisecondsSinceEpoch( |
| print.timestamp, |
| ); |
| buf.write('$timestamp: ${print.message}\n'); |
| } |
| }); |
| } |
| |
| if (plugin.lintRules.isNotEmpty) { |
| p('Lint rules:'); |
| ul(plugin.lintRules, (rule) { |
| buf.writeln(rule); |
| }); |
| } |
| if (plugin.warningRules.isNotEmpty) { |
| p('Warning rules:'); |
| ul(plugin.warningRules, (rule) { |
| buf.writeln(rule); |
| }); |
| } |
| if (plugin.fixes.isNotEmpty) { |
| p('Quick fixes:'); |
| ul(plugin.fixes, (fix) { |
| var codes = fix.codes.join(', '); |
| buf.writeln('${fix.id}: "${fix.message}" to fix $codes'); |
| }); |
| } |
| if (plugin.assists.isNotEmpty) { |
| p('Assists:'); |
| ul(plugin.assists, (assist) { |
| buf.writeln('${assist.id}: "${assist.message}"'); |
| }); |
| } |
| } |
| } |
| |
| h3('Performance:'); |
| p(''' |
| The server communicates with the plugins by sending requests. Each line |
| below provides aggregated information about one of the requests and begins with |
| the name of the request. The 'count' is the number of times the request was |
| sent. The percentiles are the 50th, 75th, 90th, 95th, and 100th percentiles of |
| the number of milliseconds it took to receiver a response. |
| '''); |
| var responseTimes = PluginManager.pluginResponseTimes[isolate] ?? {}; |
| var entries = responseTimes.entries.toList(); |
| entries.sort((first, second) => first.key.compareTo(second.key)); |
| for (var entry in entries) { |
| var requestName = entry.key; |
| var data = entry.value; |
| // TODO(brianwilkerson): Consider displaying these times as a graph, |
| // similar to the one in CompletionPage.generateContent. |
| var buffer = StringBuffer(); |
| buffer.write(requestName); |
| buffer.write(' '); |
| buffer.write(data.toAnalyticsString()); |
| p(buffer.toString()); |
| } |
| } |
| } |
| |
| void _emitTable(List<List<String>> data) { |
| buf.writeln('<table>'); |
| for (var row in data) { |
| buf.writeln('<tr>'); |
| for (var value in row) { |
| buf.writeln('<td>$value</td>'); |
| } |
| buf.writeln('</tr>'); |
| } |
| |
| buf.writeln('</table>'); |
| } |
| } |