| // Copyright (c) 2026, 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. |
| |
| part of dart._http; |
| |
| /// Collects and manages WebSocket profiling information for DevTools. |
| abstract final class WebSocketProfiler { |
| /// All active and completed WebSocket connections. |
| static final _profile = <String, _WebSocketProfileData>{}; |
| |
| static _WebSocketProfileData? startConnection( |
| int connectionId, |
| Uri uri, { |
| String? protocol, |
| }) { |
| if (const bool.fromEnvironment("dart.vm.product")) { |
| return null; |
| } |
| if (!HttpClient.enableTimelineLogging) { |
| return null; |
| } |
| |
| final connection = _WebSocketProfileData( |
| id: connectionId.toString(), |
| uri: uri, |
| protocol: protocol, |
| ); |
| |
| _profile[connection.id] = connection; |
| |
| connection.recordEvent( |
| 'WebSocket.Connect', |
| arguments: <String, Object?>{ |
| 'uri': uri.toString(), |
| 'protocol': ?protocol, |
| }, |
| ); |
| |
| return connection; |
| } |
| |
| /// Returns a profiled connection by ID. |
| static _WebSocketProfileData? getConnection(String id) { |
| return _profile[id]; |
| } |
| |
| /// Removes all stored connections. |
| static void clear() { |
| _profile.clear(); |
| } |
| |
| /// Serializes all WebSocket profile references. |
| /// Serializes all WebSocket profile references. |
| static List<Map<String, Object?>> serializeConnections(int? updatedSince) { |
| Iterable<_WebSocketProfileData> connections = _profile.values; |
| |
| if (updatedSince != null) { |
| connections = connections.where((e) => e.lastUpdateTime >= updatedSince); |
| } |
| |
| return connections.map((e) => e.toJson(ref: true)).toList(); |
| } |
| } |
| |
| /// Current lifecycle state of a WebSocket. |
| enum _WebSocketConnectionState { connecting, open, closing, closed, error } |
| |
| /// Represents one event occurring during a WebSocket lifetime. |
| class _WebSocketProfileEvent { |
| _WebSocketProfileEvent({ |
| required this.name, |
| this.arguments, |
| this.frameNumber, |
| this.direction, |
| this.opcode, |
| this.payloadSize, |
| this.errorType, |
| this.errorMessage, |
| }); |
| |
| final int timestamp = DateTime.now().microsecondsSinceEpoch; |
| final String name; |
| final int? frameNumber; |
| final String? direction; |
| final String? opcode; |
| final int? payloadSize; |
| final String? errorType; |
| final String? errorMessage; |
| final Map<String, Object?>? arguments; |
| |
| Map<String, Object?> toJson() { |
| return <String, Object?>{ |
| 'timestamp': timestamp, |
| 'event': name, |
| |
| 'frameNumber': ?frameNumber, |
| 'direction': ?direction, |
| 'opcode': ?opcode, |
| 'payloadSize': ?payloadSize, |
| 'errorType': ?errorType, |
| 'errorMessage': ?errorMessage, |
| |
| 'arguments': ?arguments, |
| }; |
| } |
| } |
| |
| /// Stores profiling information for a single WebSocket connection. |
| class _WebSocketProfileData { |
| /// Maximum number of events retained per WebSocket connection. |
| static const int _maxEventsPerConnection = 10_000; |
| |
| _WebSocketProfileData({ |
| required this.id, |
| required this.uri, |
| required this.protocol, |
| }) : connectTimestamp = DateTime.now().microsecondsSinceEpoch { |
| _updated(); |
| } |
| |
| /// Unique identifier. |
| final String id; |
| |
| /// VM isolate owning this connection. |
| static final String isolateId = Service.getIsolateId(Isolate.current)!; |
| |
| /// Remote endpoint. |
| final Uri uri; |
| |
| /// Negotiated protocol. |
| final String? protocol; |
| |
| /// Lifecycle state. |
| _WebSocketConnectionState state = .connecting; |
| |
| /// Connection timestamps. |
| final int connectTimestamp; |
| |
| int? openTimestamp; |
| int? closeTimestamp; |
| |
| /// Traffic statistics. |
| int bytesSent = 0; |
| int bytesReceived = 0; |
| int framesSent = 0; |
| int framesReceived = 0; |
| int _nextFrameNumber = 1; |
| int pingCount = 0; |
| int pongCount = 0; |
| |
| /// Close information. |
| int? closeCode; |
| String? closeReason; |
| |
| /// Error description. |
| String? error; |
| |
| /// Last update timestamp. |
| int get lastUpdateTime => _lastUpdated; |
| int _lastUpdated = 0; |
| |
| /// Event history. |
| final List<_WebSocketProfileEvent> events = <_WebSocketProfileEvent>[]; |
| |
| // Lifecycle |
| void connectionOpened() { |
| if (state != .connecting) { |
| return; |
| } |
| |
| state = .open; |
| openTimestamp = DateTime.now().microsecondsSinceEpoch; |
| |
| recordEvent( |
| 'WebSocket.Open', |
| arguments: <String, Object?>{ |
| 'uri': uri.toString(), |
| 'protocol': ?protocol, |
| }, |
| ); |
| } |
| |
| void finishConnection({ |
| required _WebSocketTrafficDirection direction, |
| int? closeCode, |
| String? reason, |
| }) { |
| if (state case .closed || .error) { |
| return; |
| } |
| |
| state = .closed; |
| |
| closeTimestamp = DateTime.now().microsecondsSinceEpoch; |
| |
| this.closeCode = closeCode; |
| closeReason = reason; |
| |
| recordEvent( |
| 'WebSocket.Close', |
| frameNumber: _nextFrameNumber++, |
| direction: direction.value, |
| opcode: _WebSocketTimelineLogger.opcodeName(_WebSocketOpcode.CLOSE), |
| payloadSize: 0, |
| arguments: <String, Object?>{?'closeCode': closeCode, ?'reason': reason}, |
| ); |
| } |
| |
| void finishWithError(Object error) { |
| if (state case .closed || .error) { |
| return; |
| } |
| |
| state = .error; |
| |
| closeTimestamp = DateTime.now().microsecondsSinceEpoch; |
| |
| this.error = error.toString(); |
| |
| recordEvent( |
| 'WebSocket.Error', |
| arguments: <String, Object?>{'error': this.error}, |
| ); |
| } |
| |
| // Traffic |
| void recordSend({required int payloadSize, required int opcode}) { |
| if (state != .open) { |
| return; |
| } |
| |
| framesSent++; |
| bytesSent += payloadSize; |
| |
| recordEvent( |
| 'WebSocket.Send', |
| frameNumber: _nextFrameNumber++, |
| direction: 'out', |
| opcode: _WebSocketTimelineLogger.opcodeName(opcode), |
| payloadSize: payloadSize, |
| arguments: <String, Object?>{ |
| 'opcode': opcode, |
| 'payloadSize': payloadSize, |
| 'framesSent': framesSent, |
| 'bytesSent': bytesSent, |
| }, |
| ); |
| } |
| |
| void recordReceive({required int payloadSize, required int opcode}) { |
| if (state != .open) { |
| return; |
| } |
| |
| framesReceived++; |
| bytesReceived += payloadSize; |
| |
| recordEvent( |
| 'WebSocket.Receive', |
| frameNumber: _nextFrameNumber++, |
| direction: 'in', |
| opcode: _WebSocketTimelineLogger.opcodeName(opcode), |
| payloadSize: payloadSize, |
| arguments: <String, Object?>{ |
| 'opcode': opcode, |
| 'payloadSize': payloadSize, |
| 'framesReceived': framesReceived, |
| 'bytesReceived': bytesReceived, |
| }, |
| ); |
| } |
| |
| void recordPing({ |
| int payloadSize = 0, |
| required _WebSocketTrafficDirection direction, |
| }) { |
| if (state != .open) { |
| return; |
| } |
| |
| pingCount++; |
| |
| recordEvent( |
| 'WebSocket.Ping', |
| frameNumber: _nextFrameNumber++, |
| direction: direction.value, |
| opcode: _WebSocketTimelineLogger.opcodeName(_WebSocketOpcode.PING), |
| payloadSize: 0, |
| arguments: <String, Object?>{'pingCount': pingCount}, |
| ); |
| } |
| |
| void recordPong({ |
| int payloadSize = 0, |
| required _WebSocketTrafficDirection direction, |
| }) { |
| if (state != .open) { |
| return; |
| } |
| |
| pongCount++; |
| |
| recordEvent( |
| 'WebSocket.Pong', |
| frameNumber: _nextFrameNumber++, |
| direction: direction.value, |
| opcode: _WebSocketTimelineLogger.opcodeName(_WebSocketOpcode.PONG), |
| payloadSize: 0, |
| arguments: <String, Object?>{'pongCount': pongCount}, |
| ); |
| } |
| |
| // Events |
| void recordEvent( |
| String name, { |
| Map<String, Object?>? arguments, |
| int? frameNumber, |
| String? direction, |
| String? opcode, |
| int? payloadSize, |
| String? errorType, |
| String? errorMessage, |
| }) { |
| events.add( |
| _WebSocketProfileEvent( |
| name: name, |
| arguments: arguments, |
| frameNumber: frameNumber, |
| direction: direction, |
| opcode: opcode, |
| payloadSize: payloadSize, |
| errorType: errorType, |
| errorMessage: errorMessage, |
| ), |
| ); |
| |
| // Prevent unbounded memory growth for long-lived connections. |
| if (events.length > _maxEventsPerConnection) { |
| events.removeAt(0); |
| } |
| |
| _updated(); |
| } |
| |
| // Serialization |
| Map<String, Object?> toJson({bool ref = false}) { |
| final json = <String, Object?>{ |
| 'type': '${ref ? '@' : ''}WebSocketConnection', |
| 'id': id, |
| 'isolateId': isolateId, |
| 'uri': uri.toString(), |
| 'state': state.name, |
| }; |
| |
| if (ref) { |
| return json; |
| } |
| |
| json.addAll(<String, Object?>{ |
| 'protocol': protocol, |
| 'connectTimestamp': connectTimestamp, |
| 'openTimestamp': openTimestamp, |
| 'closeTimestamp': closeTimestamp, |
| |
| 'bytesSent': bytesSent, |
| 'bytesReceived': bytesReceived, |
| |
| 'framesSent': framesSent, |
| 'framesReceived': framesReceived, |
| |
| 'pingCount': pingCount, |
| 'pongCount': pongCount, |
| |
| 'closeCode': closeCode, |
| 'closeReason': closeReason, |
| |
| 'error': error, |
| |
| 'events': events.map((event) => event.toJson()).toList(growable: false), |
| 'lastUpdated': _lastUpdated, |
| }); |
| |
| return json; |
| } |
| |
| void _updated() { |
| _lastUpdated = DateTime.now().microsecondsSinceEpoch; |
| } |
| } |