| // Copyright (c) 2025, 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:convert'; |
| import 'dart:developer' as developer; |
| import 'dart:io'; |
| |
| import 'package:analysis_server/protocol/protocol_constants.dart' |
| show PROTOCOL_VERSION; |
| import 'package:analysis_server/src/analysis_server.dart'; |
| import 'package:analysis_server/src/legacy_analysis_server.dart'; |
| import 'package:analysis_server/src/lsp/lsp_analysis_server.dart' |
| show LspAnalysisServer; |
| import 'package:analysis_server/src/scheduler/message_scheduler.dart'; |
| import 'package:analysis_server/src/scheduler/scheduler_tracking_listener.dart'; |
| import 'package:analysis_server/src/status/diagnostics.dart'; |
| import 'package:analysis_server/src/status/utilities/library_cycle_extensions.dart'; |
| import 'package:analysis_server/src/utilities/profiling.dart'; |
| import 'package:analysis_server_plugin/src/correction/performance.dart'; |
| import 'package:analyzer/src/dart/analysis/file_state.dart'; |
| import 'package:analyzer/src/dart/analysis/library_graph.dart'; |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; |
| import 'package:collection/collection.dart'; |
| import 'package:vm_service/vm_service_io.dart' as vm_service; |
| |
| /// Returns a json encoding of a map containing the data to be collected. |
| Future<String> collectAllData(AnalysisServer server) async { |
| Map<String, Object?> collectedData = {}; |
| |
| _collectGeneralData(collectedData, server); |
| _collectMessageSchedulerData(collectedData, server); |
| await _collectProcessData(collectedData); |
| collectCommunicationData(collectedData, server); |
| collectContextData(collectedData, server); |
| _collectAnalysisDriverData(collectedData, server); |
| _collectFileByteStoreTimingData(collectedData, server); |
| _collectPerformanceData(collectedData, server); |
| _collectExceptionsData(collectedData, server); |
| await _collectObservatoryData(collectedData); |
| |
| const JsonEncoder encoder = JsonEncoder.withIndent(' '); |
| return encoder.convert(collectedData); |
| } |
| |
| void collectCommunicationData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| for (var data in { |
| 'startup': server.performanceDuringStartup, |
| if (server.performanceAfterStartup != null) |
| 'afterStartup': server.performanceAfterStartup!, |
| }.entries) { |
| var perf = data.value; |
| var perfData = {}; |
| collectedData[data.key] = perfData; |
| |
| var requestCount = perf.requestCount; |
| var latencyCount = perf.latencyCount; |
| var averageLatency = latencyCount > 0 |
| ? (perf.requestLatency ~/ latencyCount) |
| : null; |
| var maximumLatency = perf.maxLatency; |
| var slowRequestCount = perf.slowRequestCount; |
| |
| perfData['RequestCount'] = requestCount; |
| perfData['LatencyCount'] = latencyCount; |
| perfData['AverageLatency'] = averageLatency; |
| perfData['MaximumLatency'] = maximumLatency; |
| perfData['SlowRequestCount'] = slowRequestCount; |
| } |
| } |
| |
| void collectContextData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| var driverMapValues = server.driverMap.values.toList(); |
| var contexts = []; |
| collectedData['contexts'] = contexts; |
| |
| Set<String> uniqueKnownFiles = {}; |
| for (var driver in driverMapValues) { |
| var contextData = {}; |
| contexts.add(contextData); |
| // We don't include the name because the name might include confidential |
| // information. |
| var knownFiles = driver.knownFiles.map((f) => f.path).toSet(); |
| contextData['priorityFiles'] = driver.priorityFiles.length; |
| contextData['addedFiles'] = driver.addedFiles.length; |
| contextData['knownFiles'] = knownFiles.length; |
| uniqueKnownFiles.addAll(knownFiles); |
| |
| var collectedOptionsData = collectOptionsData(driver); |
| contextData['lints'] = collectedOptionsData.lints.sorted(); |
| contextData['plugins'] = collectedOptionsData.plugins.toList(); |
| Set<LibraryCycle> cycles = {}; |
| var contextRoot = driver.analysisContext!.contextRoot; |
| var pathContext = contextRoot.resourceProvider.pathContext; |
| for (var filePath in contextRoot.analyzedFiles()) { |
| if (!file_paths.isDart(pathContext, filePath)) continue; |
| var fileState = driver.fsState.getFileForPath(filePath); |
| var kind = fileState.kind; |
| if (kind is LibraryFileKind) { |
| cycles.add(kind.libraryCycle); |
| } |
| } |
| var cycleData = <int, int>{}; |
| for (var cycle in cycles) { |
| cycleData[cycle.size] = (cycleData[cycle.size] ?? 0) + 1; |
| } |
| // Json maps need string keys. |
| var sortedCycleData = <String, int>{}; |
| for (var size in cycleData.keys.toList()..sort()) { |
| sortedCycleData['$size'] = cycleData[size]!; |
| } |
| contextData['libraryCycleData'] = sortedCycleData; |
| } |
| collectedData['uniqueKnownFiles'] = uniqueKnownFiles.length; |
| } |
| |
| void _collectAnalysisDriverData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| var buffer = StringBuffer(); |
| server.analysisDriverScheduler.accumulatedPerformance.write(buffer: buffer); |
| collectedData['accumulatedPerformance'] = buffer.toString(); |
| } |
| |
| void _collectExceptionsData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| var exceptions = []; |
| collectedData['exceptions'] = exceptions; |
| for (var exception in server.exceptions.items) { |
| exceptions.add({ |
| 'exception': exception.exception?.toString(), |
| 'fatal': exception.fatal, |
| 'message': exception.message, |
| 'stackTrace': exception.stackTrace.toString(), |
| }); |
| } |
| } |
| |
| void _collectFileByteStoreTimingData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| var byteStoreTimings = server.byteStoreTimings |
| ?.where( |
| (timing) => timing.readCount != 0 || timing.readTime != Duration.zero, |
| ) |
| .toList(); |
| if (byteStoreTimings != null && byteStoreTimings.isNotEmpty) { |
| var performance = []; |
| collectedData['byteStoreTimings'] = performance; |
| for (var i = 0; i < byteStoreTimings.length; i++) { |
| var timing = byteStoreTimings[i]; |
| if (timing.readCount == 0) { |
| continue; |
| } |
| |
| var nextTiming = i + 1 < byteStoreTimings.length |
| ? byteStoreTimings[i + 1] |
| : null; |
| var duration = (nextTiming?.time ?? DateTime.now()).difference( |
| timing.time, |
| ); |
| var value = duration.inMilliseconds; |
| var description = |
| 'Between ${timing.reason} and ${nextTiming?.reason ?? 'now'} ' |
| '(${'$value ms'}).'; |
| |
| var itemData = {}; |
| performance.add(itemData); |
| itemData['file_reads'] = timing.readCount; |
| itemData['time'] = timing.readTime.inMilliseconds; |
| itemData['description'] = description; |
| } |
| } |
| } |
| |
| void _collectGeneralData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| collectedData['currentTime'] = DateTime.now().millisecondsSinceEpoch; |
| collectedData['operatingSystem'] = Platform.operatingSystem; |
| collectedData['version'] = Platform.version; |
| collectedData['clientId'] = server.options.clientId; |
| collectedData['clientVersion'] = server.options.clientVersion; |
| collectedData['protocolVersion'] = PROTOCOL_VERSION; |
| collectedData['serverType'] = server.runtimeType.toString(); |
| collectedData['uptime'] = server.uptime.toString(); |
| if (server is LegacyAnalysisServer) { |
| collectedData['serverServices'] = server.serverServices |
| .map((e) => e.toString()) |
| .toList(); |
| } else if (server is LspAnalysisServer) { |
| collectedData['clientDiagnosticInformation'] = |
| server.clientDiagnosticInformation; |
| } |
| } |
| |
| void _collectMessageSchedulerData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| collectedData['allowOverlappingHandlers'] = |
| MessageScheduler.allowOverlappingHandlers; |
| |
| var listener = server.messageScheduler.listener; |
| if (listener is! SchedulerTrackingListener) { |
| return; |
| } |
| |
| collectedData['messageLog'] = listener.getMessageLog(); |
| } |
| |
| Future<void> _collectObservatoryData(Map<String, Object?> collectedData) async { |
| var serviceProtocolInfo = await developer.Service.getInfo(); |
| var startedServiceProtocol = false; |
| if (serviceProtocolInfo.serverUri == null) { |
| startedServiceProtocol = true; |
| serviceProtocolInfo = await developer.Service.controlWebServer( |
| enable: true, |
| silenceOutput: true, |
| ); |
| } |
| |
| var serverUri = serviceProtocolInfo.serverUri; |
| if (serverUri != null) { |
| var path = serverUri.path; |
| if (!path.endsWith('/')) path += '/'; |
| var wsUriString = 'ws://${serverUri.authority}${path}ws'; |
| var serviceClient = await vm_service.vmServiceConnectUri(wsUriString); |
| var vm = await serviceClient.getVM(); |
| collectedData['vm.architectureBits'] = vm.architectureBits; |
| collectedData['vm.hostCPU'] = vm.hostCPU; |
| collectedData['vm.operatingSystem'] = vm.operatingSystem; |
| collectedData['vm.startTime'] = vm.startTime; |
| |
| var processMemoryUsage = await serviceClient.getProcessMemoryUsage(); |
| collectedData['processMemoryUsage'] = processMemoryUsage.json; |
| |
| var isolateData = []; |
| collectedData['isolates'] = isolateData; |
| var isolates = [...?vm.isolates, ...?vm.systemIsolates]; |
| for (var isolate in isolates) { |
| String? id = isolate.id; |
| if (id == null) continue; |
| var thisIsolateData = {}; |
| isolateData.add(thisIsolateData); |
| thisIsolateData['id'] = id; |
| thisIsolateData['isolateGroupId'] = isolate.isolateGroupId; |
| thisIsolateData['name'] = isolate.name; |
| var isolateMemoryUsage = await serviceClient.getMemoryUsage(id); |
| thisIsolateData['memory'] = isolateMemoryUsage.json; |
| var allocationProfile = await serviceClient.getAllocationProfile(id); |
| var allocationMembers = allocationProfile.members ?? []; |
| var allocationProfileData = <Map<String, Object?>>[]; |
| thisIsolateData['allocationProfile'] = allocationProfileData; |
| for (var member in allocationMembers) { |
| var bytesCurrent = member.bytesCurrent; |
| // Filter out very small entries to avoid the report becoming too big. |
| if (bytesCurrent == null || bytesCurrent < 1024) continue; |
| |
| var memberData = <String, Object?>{}; |
| allocationProfileData.add(memberData); |
| memberData['bytesCurrent'] = bytesCurrent; |
| memberData['instancesCurrent'] = member.instancesCurrent; |
| memberData['accumulatedSize'] = member.accumulatedSize; |
| memberData['instancesAccumulated'] = member.instancesAccumulated; |
| memberData['className'] = member.classRef?.name; |
| memberData['libraryName'] = member.classRef?.library?.name; |
| } |
| allocationProfileData.sort((a, b) { |
| int bytesCurrentA = a['bytesCurrent'] as int; |
| int bytesCurrentB = b['bytesCurrent'] as int; |
| // Largest first. |
| return bytesCurrentB.compareTo(bytesCurrentA); |
| }); |
| } |
| } |
| |
| if (startedServiceProtocol) { |
| await developer.Service.controlWebServer(silenceOutput: true); |
| } |
| } |
| |
| void _collectPerformanceData( |
| Map<String, Object?> collectedData, |
| AnalysisServer server, |
| ) { |
| collectedData['performanceCompletion'] = _convertPerformance( |
| server.recentPerformance.completion.items.toList(), |
| ); |
| collectedData['performanceGetFixes'] = _convertPerformance( |
| server.recentPerformance.getFixes.items.toList(), |
| ); |
| collectedData['performanceRequests'] = _convertPerformance( |
| server.recentPerformance.requests.items.toList(), |
| ); |
| collectedData['performanceSlowRequests'] = _convertPerformance( |
| server.recentPerformance.slowRequests.items.toList(), |
| ); |
| } |
| |
| Future<void> _collectProcessData(Map<String, Object?> collectedData) async { |
| var profiler = ProcessProfiler.getProfilerForPlatform(); |
| UsageInfo? usage; |
| if (profiler != null) { |
| usage = await profiler.getProcessUsage(pid); |
| } |
| collectedData['memoryKB'] = usage?.memoryKB; |
| collectedData['cpuPercentage'] = usage?.cpuPercentage; |
| collectedData['currentRss'] = ProcessInfo.currentRss; |
| collectedData['maxRss'] = ProcessInfo.maxRss; |
| } |
| |
| // Recorded performance data (timing and code completion). |
| List<Object> _convertPerformance(List<RequestPerformance> items) { |
| var performance = <Object>[]; |
| |
| for (var item in items) { |
| var itemData = {}; |
| performance.add(itemData); |
| |
| itemData['id'] = item.id; |
| itemData['operation'] = item.operation; |
| itemData['requestLatency'] = item.requestLatency; |
| itemData['elapsed'] = item.performance.elapsed.inMilliseconds; |
| itemData['startTime'] = item.startTime?.toIso8601String(); |
| |
| var buffer = StringBuffer(); |
| item.performance.write(buffer: buffer); |
| itemData['performance'] = buffer.toString(); |
| } |
| return performance; |
| } |