1707f11db4
Users report sporadic failure on Windows ARM64 but I am unable to reproduce this locally. Fixes https://github.com/dart-lang/sdk/issues/63343 TEST=Unable to reproduce the underlying issue Change-Id: I78c0be78f5e70755b69fec50a896aaef6a6a6964 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502060 Reviewed-by: Martin Kustermann <kustermann@google.com> Commit-Queue: Slava Egorov <vegorov@google.com>
274 lines
8.6 KiB
Dart
274 lines
8.6 KiB
Dart
// 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:async';
|
|
import 'dart:developer' as developer;
|
|
import 'dart:ffi' as ffi;
|
|
import 'dart:io' as io;
|
|
|
|
import 'package:ffi/ffi.dart' show calloc;
|
|
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
|
|
|
import 'src/common.dart';
|
|
import 'src/json_rpc.dart';
|
|
import 'src/process_info.dart';
|
|
|
|
class PerfWitnessServer {
|
|
final String? _tag;
|
|
final String _controlSocketPath;
|
|
final String _recorderSocketPath;
|
|
final ffi.Pointer<ffi.Bool> _isRecordingTimelineWithAsyncSpans;
|
|
|
|
json_rpc.Peer? _timelineRequestor;
|
|
|
|
JsonRpcServer? _server;
|
|
json_rpc.Peer? _recorderConnection;
|
|
|
|
static ffi.Pointer<ffi.Bool>? _sharedIsRecordingTimelineWithAsyncSpans;
|
|
|
|
late final Map<String, JsonRpcMethod> _methods = {
|
|
'process.getInfo': _getProcessInfo,
|
|
'timeline.streamTo': _timelineStreamTo,
|
|
'timeline.stopStreaming': _timelineStopStreaming,
|
|
'process._isRecordingTimelineWithAsyncSpansAddr':
|
|
_isRecordingTimelineWithAsyncSpansAddr,
|
|
};
|
|
|
|
static PerfWitnessServer? _instance;
|
|
|
|
PerfWitnessServer._(
|
|
this._tag,
|
|
this._controlSocketPath,
|
|
this._recorderSocketPath,
|
|
) : _isRecordingTimelineWithAsyncSpans = calloc(ffi.sizeOf<ffi.Bool>());
|
|
|
|
/// Starts [PerfWitnessServer] instance if necessary.
|
|
///
|
|
/// This does nothing if there is already another instance running in the
|
|
/// same process.
|
|
///
|
|
/// If [inBackground] is `true` then server start will happen
|
|
/// concurrently in background and the returned [Future] will compelete
|
|
/// immediately, unless there is a recorder process monitoring start
|
|
/// of new processes.
|
|
static Future<void> start({
|
|
String? tag,
|
|
bool inBackground = false,
|
|
String? socketPath,
|
|
}) {
|
|
if (_instance == null) {
|
|
try {
|
|
final started = _startImpl(tag: tag, socketPath: socketPath);
|
|
if (inBackground && !_isRecorderProcessPossiblyActive) {
|
|
started.ignore();
|
|
} else {
|
|
return started;
|
|
}
|
|
} catch (_) {
|
|
// Ignore any issues with starting up.
|
|
}
|
|
}
|
|
|
|
return Future.value();
|
|
}
|
|
|
|
static bool get _isRecorderProcessPossiblyActive {
|
|
if (recorderSocketPath case final path?) {
|
|
return io.FileSystemEntity.typeSync(path) == .unixDomainSock;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static Future<void> _startImpl({
|
|
required String? tag,
|
|
required String? socketPath,
|
|
}) async {
|
|
assert(_instance == null);
|
|
|
|
socketPath ??= controlSocketPath;
|
|
if (socketPath != null) {
|
|
if (io.FileSystemEntity.typeSync(socketPath) == .unixDomainSock) {
|
|
try {
|
|
// Another isolate is already serving the process. We assume that
|
|
// server will remain open as long as the process is running.
|
|
// However we want to make sure that setting global settings (e.g.
|
|
// whether async spans are enabled or not) will affect all isolates
|
|
// not just the one that created the server.
|
|
final client = jsonRpcPeerFromSocket(
|
|
await UnixDomainSocket.connect(socketPath),
|
|
);
|
|
try {
|
|
final {'address': int addr, 'pid': int pid} =
|
|
await client.sendRequest(
|
|
'process._isRecordingTimelineWithAsyncSpansAddr',
|
|
)
|
|
as Map<String, dynamic>;
|
|
// Just double check that we are the very same process.
|
|
if (pid != io.pid) {
|
|
return;
|
|
}
|
|
_sharedIsRecordingTimelineWithAsyncSpans = .fromAddress(addr);
|
|
return;
|
|
} finally {
|
|
client.close().ignore(); // Don't leave the client open.
|
|
}
|
|
} catch (_) {
|
|
// Socket looks defunct, delete it and continue.
|
|
try {
|
|
io.File(socketPath).deleteSync();
|
|
} catch (_) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
_instance = PerfWitnessServer._(tag, socketPath, recorderSocketPath!);
|
|
await _instance!._start();
|
|
}
|
|
}
|
|
|
|
static Future<void> shutdown() async {
|
|
await _instance?._shutdown();
|
|
_instance = null;
|
|
}
|
|
|
|
static bool get isRecordingTimelineWithAsyncSpans {
|
|
return _sharedIsRecordingTimelineWithAsyncSpans?.value ?? false;
|
|
}
|
|
|
|
Future<void> _timelineStreamTo(
|
|
json_rpc.Peer requestor,
|
|
Map<String, Object?>? params,
|
|
) async {
|
|
if (_timelineRequestor != null) {
|
|
throw StateError('Timeline is already being recorded');
|
|
}
|
|
|
|
final paramsObj = StreamTimelineToRequest(params ?? {});
|
|
|
|
final streams = paramsObj.streams
|
|
?.map((s) => developer.TimelineStream.values.byName(s))
|
|
.toList();
|
|
|
|
final samplingIntervalUs = paramsObj.samplingInterval;
|
|
final samplingInterval = samplingIntervalUs != null
|
|
? Duration(microseconds: samplingIntervalUs)
|
|
: const Duration(microseconds: 1000);
|
|
|
|
final enableAsyncSpans = paramsObj.enableAsyncSpans ?? false;
|
|
|
|
developer.NativeRuntime.streamTimelineTo(
|
|
developer.TimelineRecorder.values.byName(paramsObj.recorder),
|
|
path: paramsObj.path,
|
|
streams:
|
|
streams ??
|
|
const [developer.TimelineStream.dart, developer.TimelineStream.gc],
|
|
enableProfiler: paramsObj.enableProfiler ?? false,
|
|
samplingInterval: samplingInterval,
|
|
);
|
|
_timelineRequestor = requestor;
|
|
requestor.done.whenComplete(() {
|
|
if (_timelineRequestor == requestor) {
|
|
developer.NativeRuntime.stopStreamingTimeline();
|
|
_isRecordingTimelineWithAsyncSpans.value = false;
|
|
_timelineRequestor = null;
|
|
}
|
|
}).ignore();
|
|
_isRecordingTimelineWithAsyncSpans.value = enableAsyncSpans;
|
|
}
|
|
|
|
Future<void> _timelineStopStreaming(
|
|
json_rpc.Peer requestor,
|
|
Map<String, Object?>? params,
|
|
) async {
|
|
if (_timelineRequestor == null) {
|
|
throw StateError('Timeline is not being recorded');
|
|
}
|
|
|
|
if (_timelineRequestor != requestor) {
|
|
throw StateError('This peer did not start recording timeline');
|
|
}
|
|
|
|
_timelineRequestor = null;
|
|
developer.NativeRuntime.stopStreamingTimeline();
|
|
_isRecordingTimelineWithAsyncSpans.value = false;
|
|
}
|
|
|
|
Future<Map<String, Object?>> _getProcessInfo(
|
|
json_rpc.Peer requestor,
|
|
Map<String, Object?>? params,
|
|
) async {
|
|
return ProcessInfo.current(tag: _tag).toJson();
|
|
}
|
|
|
|
Future<Map<String, Object?>> _isRecordingTimelineWithAsyncSpansAddr(
|
|
json_rpc.Peer requestor,
|
|
Map<String, Object?>? params,
|
|
) async {
|
|
return {
|
|
'address': _isRecordingTimelineWithAsyncSpans.address,
|
|
'pid': io.pid,
|
|
};
|
|
}
|
|
|
|
Future<void> _start() async {
|
|
_sharedIsRecordingTimelineWithAsyncSpans =
|
|
_isRecordingTimelineWithAsyncSpans;
|
|
_server = JsonRpcServer(
|
|
await UnixDomainSocket.bind(_controlSocketPath),
|
|
_methods,
|
|
);
|
|
await _announceProcessTo(
|
|
_recorderSocketPath,
|
|
_tag,
|
|
).timeout(Duration(milliseconds: 100), onTimeout: () => Future.value());
|
|
}
|
|
|
|
Future<void> _announceProcessTo(String recorderPath, String? tag) async {
|
|
try {
|
|
_recorderConnection = jsonRpcPeerFromSocket(
|
|
await UnixDomainSocket.connect(recorderPath),
|
|
_methods,
|
|
);
|
|
_recorderConnection!.done.whenComplete(() {
|
|
_recorderConnection = null;
|
|
}).ignore();
|
|
await _recorderConnection!.sendRequest(
|
|
'process.announce',
|
|
ProcessInfo.current(tag: tag).toJson(),
|
|
);
|
|
} catch (e) {
|
|
// ignore, recorder might not be running.
|
|
}
|
|
}
|
|
|
|
Future<void> _shutdown() async {
|
|
_recorderConnection?.close();
|
|
if (_timelineRequestor != null) {
|
|
developer.NativeRuntime.stopStreamingTimeline();
|
|
_timelineRequestor = null;
|
|
}
|
|
await _server?.close();
|
|
if (io.FileSystemEntity.typeSync(_controlSocketPath) != .notFound) {
|
|
try {
|
|
// Normally we would not expect this to throw, but on Windows
|
|
// users reported an occasional ERROR_CANT_ACCESS_FILE thrown here,
|
|
// possibly due to some sort of NTFS race with deleting the reparse
|
|
// point after unix domain socket was closed. See dartbug.com/63343.
|
|
io.File(_controlSocketPath).deleteSync();
|
|
} catch (_) {}
|
|
}
|
|
calloc.free(_isRecordingTimelineWithAsyncSpans);
|
|
}
|
|
}
|
|
|
|
extension type StreamTimelineToRequest(Map<String, Object?> json) {
|
|
String get recorder => json['recorder'] as String;
|
|
String? get path => json['path'] as String?;
|
|
List<String>? get streams => (json['streams'] as List?)?.cast<String>();
|
|
bool? get enableProfiler => json['enableProfiler'] as bool?;
|
|
int? get samplingInterval => json['samplingInterval'] as int?;
|
|
bool? get enableAsyncSpans => json['enableAsyncSpans'] as bool?;
|
|
}
|