Skip to content

Commit 2b5e090

Browse files
authored
Refactor android replay recorder to use new isolate worker class (#3296)
* Update * Update
1 parent 6ad8fc4 commit 2b5e090

File tree

4 files changed

+72
-113
lines changed

4 files changed

+72
-113
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
- Mark file sync spans run in the main isolate with `blocked_main_thread` ([#3270](https://github.com/getsentry/sentry-dart/pull/3270))
88
- This allows Sentry to create issues automatically out of file spans running a certain time on the main thread: https://docs.sentry.io/product/issues/issue-details/performance-issues/file-main-thread-io/
99

10-
## 9.7.0
10+
### Enhancements
1111

12+
- Refactor `AndroidReplayRecorder` to use the new worker isolate api [#3296](https://github.com/getsentry/sentry-dart/pull/3296/)
13+
- Offload `captureEnvelope` to background isolate for Cocoa and Android [#3232](https://github.com/getsentry/sentry-dart/pull/3232)
14+
15+
## 9.7.0
1216

1317
### Features
1418

packages/flutter/lib/src/isolate/isolate_worker.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'dart:isolate';
44
import '../../sentry_flutter.dart';
55
import 'isolate_logger.dart';
66

7+
typedef SpawnWorkerFn = Future<Worker> Function(WorkerConfig, WorkerEntry);
8+
79
const _shutdownCommand = '_shutdown_';
810

911
// -------------------------------------------

packages/flutter/lib/src/native/java/android_envelope_sender.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import '../../isolate/isolate_worker.dart';
1010
import '../../isolate/isolate_logger.dart';
1111
import 'binding.dart' as native;
1212

13-
typedef SpawnWorkerFn = Future<Worker> Function(WorkerConfig, WorkerEntry);
14-
1513
class AndroidEnvelopeSender {
1614
final SentryFlutterOptions _options;
1715
final WorkerConfig _config;

packages/flutter/lib/src/native/java/android_replay_recorder.dart

Lines changed: 65 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:jni/jni.dart';
66
import 'package:meta/meta.dart';
77

88
import '../../../sentry_flutter.dart';
9+
import '../../isolate/isolate_logger.dart';
10+
import '../../isolate/isolate_worker.dart';
911
import '../../replay/scheduled_recorder.dart';
1012
import '../../screenshot/screenshot.dart';
1113
import 'binding.dart' as native;
@@ -14,21 +16,30 @@ import 'binding.dart' as native;
1416
// cumbersome, see https://github.com/dart-lang/native/issues/1794
1517
@internal
1618
class AndroidReplayRecorder extends ScheduledScreenshotRecorder {
17-
_AndroidNativeReplayWorker? _worker;
19+
final WorkerConfig _config;
20+
final SpawnWorkerFn _spawn;
21+
Worker? _worker;
1822

1923
@internal // visible for testing, used by SentryNativeJava
2024
static AndroidReplayRecorder Function(SentryFlutterOptions) factory =
2125
AndroidReplayRecorder.new;
2226

23-
AndroidReplayRecorder(super.options) {
27+
AndroidReplayRecorder(super.options, {SpawnWorkerFn? spawn})
28+
: _config = WorkerConfig(
29+
debugName: 'SentryAndroidReplayRecorder',
30+
debug: options.debug,
31+
diagnosticLevel: options.diagnosticLevel,
32+
automatedTestMode: options.automatedTestMode,
33+
),
34+
_spawn = spawn ?? spawnWorker {
2435
super.callback = _addReplayScreenshot;
2536
}
2637

2738
@override
2839
Future<void> start() async {
29-
final spawningWorker = _AndroidNativeReplayWorker.spawn();
40+
if (_worker != null) return;
41+
_worker = await _spawn(_config, _entryPoint);
3042
await super.start();
31-
_worker = await spawningWorker;
3243
}
3344

3445
@override
@@ -50,7 +61,7 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder {
5061
'${screenshot.width}x${screenshot.height} pixels, '
5162
'${data.lengthInBytes} bytes)');
5263

53-
await _worker!.nativeAddScreenshot(_WorkItem(
64+
await _worker!.request(_WorkItem(
5465
timestamp: timestamp,
5566
data: data.buffer.asUint8List(),
5667
width: screenshot.width,
@@ -68,127 +79,71 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder {
6879
}
6980
}
7081
}
71-
}
72-
73-
// Based on https://dart.dev/language/isolates#robust-ports-example
74-
class _AndroidNativeReplayWorker {
75-
final SendPort _commands;
76-
final ReceivePort _responses;
77-
final Map<int, Completer<Object?>> _activeRequests = {};
78-
int _idCounter = 0;
79-
bool _closed = false;
80-
81-
static Future<_AndroidNativeReplayWorker> spawn() async {
82-
// Create a receive port and add its initial message handler
83-
final initPort = RawReceivePort();
84-
final connection = Completer<(ReceivePort, SendPort)>.sync();
85-
initPort.handler = (SendPort commandPort) {
86-
connection.complete((
87-
ReceivePort.fromRawReceivePort(initPort),
88-
commandPort,
89-
));
90-
};
91-
92-
// Spawn the isolate.
93-
try {
94-
await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort),
95-
debugName: 'SentryReplayRecorder');
96-
} on Object {
97-
initPort.close();
98-
rethrow;
99-
}
100-
101-
final (ReceivePort receivePort, SendPort sendPort) =
102-
await connection.future;
10382

104-
return _AndroidNativeReplayWorker._(receivePort, sendPort);
83+
static void _entryPoint((SendPort, WorkerConfig) init) {
84+
final (host, config) = init;
85+
runWorker(config, host, _AndroidReplayHandler(config));
10586
}
87+
}
10688

107-
_AndroidNativeReplayWorker._(this._responses, this._commands) {
108-
_responses.listen(_handleResponsesFromIsolate);
109-
}
89+
class _AndroidReplayHandler extends WorkerHandler {
90+
final WorkerConfig _config;
91+
// Android Bitmap creation is a bit costly so we reuse it between captures.
92+
native.Bitmap? _bitmap;
93+
late final native.ReplayIntegration _nativeReplay;
11094

111-
Future<Object?> nativeAddScreenshot(_WorkItem item) async {
112-
if (_closed) throw StateError('Closed');
113-
final completer = Completer<Object?>.sync();
114-
final id = _idCounter++;
115-
_activeRequests[id] = completer;
116-
_commands.send((id, item));
117-
return await completer.future;
95+
_AndroidReplayHandler(this._config) {
96+
_nativeReplay = native.SentryFlutterPlugin.Companion
97+
.privateSentryGetReplayIntegration()!;
11898
}
11999

120-
void _handleResponsesFromIsolate(dynamic message) {
121-
final (int id, Object? response) = message as (int, Object?);
122-
final completer = _activeRequests.remove(id)!;
123-
124-
if (response is RemoteError) {
125-
completer.completeError(response);
126-
} else {
127-
completer.complete(response);
128-
}
129-
130-
if (_closed && _activeRequests.isEmpty) _responses.close();
100+
@override
101+
FutureOr<void> onMessage(Object? message) {
102+
IsolateLogger.log(
103+
SentryLevel.warning, 'Unexpected fire-and-forget message: $message');
131104
}
132105

133-
/// This is the actual Android native implementation, the rest is just plumbing.
134-
static void _handleCommandsToIsolate(
135-
ReceivePort receivePort,
136-
SendPort sendPort,
137-
) {
138-
// Android Bitmap creation is a bit costly so we reuse it between captures.
139-
native.Bitmap? bitmap;
106+
@override
107+
FutureOr<Object?> onRequest(Object? payload) {
108+
if (payload is! _WorkItem) {
109+
IsolateLogger.log(
110+
SentryLevel.warning, 'Unexpected payload type: $payload');
111+
return null;
112+
}
140113

141-
final _nativeReplay = native.SentryFlutterPlugin.Companion
142-
.privateSentryGetReplayIntegration()!;
114+
final item = payload;
115+
JByteBuffer? jBuffer;
143116

144-
receivePort.listen((message) {
145-
if (message == 'shutdown') {
146-
receivePort.close();
147-
return;
148-
}
149-
final (id, item) = message as (int, _WorkItem);
150-
try {
151-
if (bitmap != null) {
152-
if (bitmap!.getWidth() != item.width ||
153-
bitmap!.getHeight() != item.height) {
154-
bitmap!.release();
155-
bitmap = null;
156-
}
117+
try {
118+
if (_bitmap != null) {
119+
if (_bitmap!.getWidth() != item.width ||
120+
_bitmap!.getHeight() != item.height) {
121+
_bitmap!.release();
122+
_bitmap = null;
157123
}
124+
}
158125

159-
// https://developer.android.com/reference/android/graphics/Bitmap#createBitmap(int,%20int,%20android.graphics.Bitmap.Config)
160-
// Note: while the generated API is nullable, the docs say the returned value cannot be null..
161-
bitmap ??= native.Bitmap.createBitmap$3(
162-
item.width, item.height, native.Bitmap$Config.ARGB_8888);
126+
// https://developer.android.com/reference/android/graphics/Bitmap#createBitmap(int,%20int,%20android.graphics.Bitmap.Config)
127+
// Note: while the generated API is nullable, the docs say the returned value cannot be null..
128+
_bitmap ??= native.Bitmap.createBitmap$3(
129+
item.width, item.height, native.Bitmap$Config.ARGB_8888);
163130

164-
final jBuffer = JByteBuffer.fromList(item.data);
165-
try {
166-
bitmap!.copyPixelsFromBuffer(jBuffer);
167-
} finally {
168-
jBuffer.release();
169-
}
131+
jBuffer = JByteBuffer.fromList(item.data);
132+
_bitmap!.copyPixelsFromBuffer(jBuffer);
170133

171-
// TODO timestamp is currently missing in onScreenshotRecorded()
172-
_nativeReplay.onScreenshotRecorded(bitmap!);
134+
// TODO timestamp is currently missing in onScreenshotRecorded()
135+
_nativeReplay.onScreenshotRecorded(_bitmap!);
173136

174-
sendPort.send((id, null));
175-
} catch (e, stacktrace) {
176-
sendPort.send((id, RemoteError(e.toString(), stacktrace.toString())));
137+
return null;
138+
} catch (exception, stackTrace) {
139+
IsolateLogger.log(SentryLevel.error, 'Failed to add replay screenshot',
140+
exception: exception, stackTrace: stackTrace);
141+
if (_config.automatedTestMode) {
142+
rethrow;
177143
}
178-
});
179-
}
180-
181-
static void _startRemoteIsolate(SendPort sendPort) {
182-
final receivePort = ReceivePort();
183-
sendPort.send(receivePort.sendPort);
184-
_handleCommandsToIsolate(receivePort, sendPort);
185-
}
186-
187-
void close() {
188-
if (!_closed) {
189-
_closed = true;
190-
_commands.send('shutdown');
191-
if (_activeRequests.isEmpty) _responses.close();
144+
return null;
145+
} finally {
146+
jBuffer?.release();
192147
}
193148
}
194149
}

0 commit comments

Comments
 (0)