Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2b514dd
capture screenshots in sequence until they're stable
vaind Jan 2, 2025
d0c6c01
improve timestamps
vaind Jan 2, 2025
6f0f351
fix tests & future handling
vaind Jan 7, 2025
8bc4df7
example: debug log only in debug mode
vaind Jan 7, 2025
c990fe2
formatting
vaind Jan 7, 2025
7636b2c
min-version compat
vaind Jan 7, 2025
ae34a38
linter issue
vaind Jan 8, 2025
9bedc01
fix scheduler tests
vaind Jan 8, 2025
32f5b35
cleanup
vaind Jan 8, 2025
9d4a3f9
fix: scheduler test
vaind Jan 8, 2025
4439231
Merge branch 'main' into fix/screenshot-masking-during-changes
vaind Jan 8, 2025
8550861
improve performance
vaind Jan 9, 2025
05574bb
improve screenshot comparison performance
vaind Jan 10, 2025
ae1c567
comments
vaind Jan 10, 2025
d010424
screenshot list-equal fix & tests
vaind Jan 10, 2025
f62e5ee
min-version failure
vaind Jan 10, 2025
b78c583
rename retrier to stabilizer
vaind Jan 10, 2025
9509e44
chore: changelog
vaind Jan 10, 2025
46acddd
Merge branch 'main' into fix/screenshot-masking-during-changes
vaind Jan 10, 2025
0418629
capture stable issue screenshots when masking is enabled
vaind Jan 10, 2025
183e995
fixes and tests
vaind Jan 10, 2025
4ccd5ea
better logging
vaind Jan 10, 2025
e7ab100
cocoa replay fix
vaind Jan 10, 2025
622d859
fix dart2js tests
vaind Jan 10, 2025
e9cebf5
fix screenshot capture hanging
vaind Jan 10, 2025
07aec1d
fix integration test
vaind Jan 11, 2025
134b015
time out if we can't take a screenshot for events
vaind Jan 11, 2025
7f1fbf4
enha: use rgba for cocoa replay
vaind Jan 11, 2025
9d6fbb7
Merge remote-tracking branch 'origin/main' into feat/replay-bitmap-tr…
vaind Jan 13, 2025
c624aa6
chore: improve docs
vaind Jan 13, 2025
69e689c
chore: update changelog
vaind Jan 13, 2025
219d04d
Merge branch 'main' into feat/replay-bitmap-transfer
vaind Jan 14, 2025
b9774d4
Merge branch 'main' into feat/replay-bitmap-transfer
vaind Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

### Enhancements

- Replay: improve iOS native interop performance ([#2530](https://github.com/getsentry/sentry-dart/pull/2530))
- Replay: improve iOS native interop performance ([#2530](https://github.com/getsentry/sentry-dart/pull/2530), [#2573](https://github.com/getsentry/sentry-dart/pull/2573))
- Replay: improve orientation change tracking accuracy on Android ([#2540](https://github.com/getsentry/sentry-dart/pull/2540))

### Fixes
Expand Down
36 changes: 35 additions & 1 deletion flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,45 @@ - (void)imageWithView:(UIView *_Nonnull)view
NSDictionary *dict = (NSDictionary *)value;
long address = ((NSNumber *)dict[@"address"]).longValue;
NSNumber *length = ((NSNumber *)dict[@"length"]);
NSNumber *width = ((NSNumber *)dict[@"width"]);
NSNumber *height = ((NSNumber *)dict[@"height"]);
NSData *data =
[NSData dataWithBytesNoCopy:(void *)address
length:length.unsignedLongValue
freeWhenDone:TRUE];
UIImage *image = [UIImage imageWithData:data];

// We expect rawRGBA, see docs for ImageByteFormat:
// https://api.flutter.dev/flutter/dart-ui/ImageByteFormat.html
// Unencoded bytes, in RGBA row-primary form with premultiplied
// alpha, 8 bits per channel.
static const int kBitsPerChannel = 8;
static const int kBytesPerPixel = 4;
assert(length.unsignedLongValue % kBytesPerPixel == 0);

// Let's create an UIImage from the raw data.
// We need to provide it the width & height and
// the info how the data is encoded.
CGDataProviderRef provider =
CGDataProviderCreateWithCFData((CFDataRef)data);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo =
kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast;
CGImageRef cgImage = CGImageCreate(
width.unsignedLongValue, // width
height.unsignedLongValue, // height
kBitsPerChannel, // bits per component
kBitsPerChannel * kBytesPerPixel, // bits per pixel
width.unsignedLongValue * kBytesPerPixel, // bytes per row
colorSpace, bitmapInfo, provider, NULL, false,
kCGRenderingIntentDefault);

UIImage *image = [UIImage imageWithCGImage:cgImage];

// UIImage takes its own refs, we need to release these here.
CGImageRelease(cgImage);
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(provider);

onComplete(image);
return;
} else if ([value isKindOfClass:[FlutterError class]]) {
Expand Down
19 changes: 12 additions & 7 deletions flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:meta/meta.dart';

Expand All @@ -8,30 +7,36 @@ import '../../replay/replay_recorder.dart';
import '../../screenshot/recorder.dart';
import '../../screenshot/recorder_config.dart';
import '../../screenshot/stabilizer.dart';
import '../native_memory.dart';

@internal
class CocoaReplayRecorder {
final SentryFlutterOptions _options;
final ScreenshotRecorder _recorder;
late final ScreenshotStabilizer<void> _stabilizer;
var _completer = Completer<Uint8List?>();
var _completer = Completer<Map<String, int>?>();

CocoaReplayRecorder(this._options)
: _recorder =
ReplayScreenshotRecorder(ScreenshotRecorderConfig(), _options) {
_stabilizer = ScreenshotStabilizer(_recorder, _options, (screenshot) async {
final pngData = await screenshot.pngData;
final data = await screenshot.rawRgbaData;
_options.logger(
SentryLevel.debug,
'Replay: captured screenshot ('
'${screenshot.width}x${screenshot.height} pixels, '
'${pngData.lengthInBytes} bytes)');
_completer.complete(pngData.buffer.asUint8List());
'${data.lengthInBytes} bytes)');

// Malloc memory and copy the data. Native must free it.
final json = data.toNativeMemory().toJson();
json['width'] = screenshot.width;
json['height'] = screenshot.height;
_completer.complete(json);
});
}

Future<Uint8List?> captureScreenshot() async {
_completer = Completer<Uint8List?>();
Future<Map<String, int>?> captureScreenshot() async {
_completer = Completer();
_stabilizer.ensureFrameAndAddCallback((msSinceEpoch) {
_stabilizer.capture(msSinceEpoch).onError(_completer.completeError);
});
Expand Down
5 changes: 1 addition & 4 deletions flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../replay/replay_config.dart';
import '../native_memory.dart';
import '../sentry_native_channel.dart';
import 'binding.dart' as cocoa;
import 'cocoa_replay_recorder.dart';
Expand Down Expand Up @@ -43,9 +42,7 @@ class SentryNativeCocoa extends SentryNativeChannel {
});
}

final uint8List = await _replayRecorder!.captureScreenshot();
// Malloc memory and copy the data. Native must free it.
return uint8List?.toNativeMemory().toJson();
return _replayRecorder!.captureScreenshot();
default:
throw UnimplementedError('Method ${call.method} not implemented');
}
Expand Down
27 changes: 19 additions & 8 deletions flutter/lib/src/native/native_memory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,24 @@ class NativeMemory {

const NativeMemory._(this.pointer, this.length);

factory NativeMemory.fromUint8List(Uint8List source) {
final length = source.length;
final ptr = pkg_ffi.malloc.allocate<Uint8>(length);
if (length > 0) {
ptr.asTypedList(length).setAll(0, source);
factory NativeMemory.fromByteData(ByteData source) {
final lengthInBytes = source.lengthInBytes;
final ptr = pkg_ffi.malloc.allocate<Uint8>(lengthInBytes);

// TODO memcpy() from source.buffer.asUint8List().address
// once we can depend on Dart SDK 3.5+
final numWords = lengthInBytes ~/ 8;
final words = ptr.cast<Uint64>().asTypedList(numWords);
if (numWords > 0) {
words.setAll(0, source.buffer.asUint64List(0, numWords));
}
return NativeMemory._(ptr, length);

final bytes = ptr.asTypedList(lengthInBytes);
for (var i = words.lengthInBytes; i < source.lengthInBytes; i++) {
bytes[i] = source.getUint8(i);
}

return NativeMemory._(ptr, lengthInBytes);
}

factory NativeMemory.fromJson(Map<dynamic, dynamic> json) {
Expand All @@ -44,6 +55,6 @@ class NativeMemory {
}

@internal
extension Uint8ListNativeMemory on Uint8List {
NativeMemory toNativeMemory() => NativeMemory.fromUint8List(this);
extension ByteDataNativeMemory on ByteData {
NativeMemory toNativeMemory() => NativeMemory.fromByteData(this);
}
16 changes: 8 additions & 8 deletions flutter/lib/src/screenshot/screenshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ class Screenshot {
final Image _image;
final DateTime timestamp;
final Flow flow;
Future<ByteData>? _rawData;
Future<ByteData>? _rawRgbaData;
Future<ByteData>? _pngData;
bool _disposed = false;

Screenshot(this._image, this.timestamp, this.flow);
Screenshot._cloned(
this._image, this.timestamp, this.flow, this._rawData, this._pngData);
this._image, this.timestamp, this.flow, this._rawRgbaData, this._pngData);

int get width => _image.width;
int get height => _image.height;

Future<ByteData> get rawData {
_rawData ??= _encode(ImageByteFormat.rawUnmodified);
return _rawData!;
Future<ByteData> get rawRgbaData {
_rawRgbaData ??= _encode(ImageByteFormat.rawRgba);
return _rawRgbaData!;
}

Future<ByteData> get pngData {
Expand All @@ -46,20 +46,20 @@ class Screenshot {
return false;
}

return listEquals(await rawData, await other.rawData);
return listEquals(await rawRgbaData, await other.rawRgbaData);
}

Screenshot clone() {
assert(!_disposed, 'Cannot clone a disposed screenshot');
return Screenshot._cloned(
_image.clone(), timestamp, flow, _rawData, _pngData);
_image.clone(), timestamp, flow, _rawRgbaData, _pngData);
}

void dispose() {
if (!_disposed) {
_disposed = true;
_image.dispose();
_rawData = null;
_rawRgbaData = null;
_pngData = null;
}
}
Expand Down
13 changes: 7 additions & 6 deletions flutter/test/native_memory_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,30 @@ import 'native_memory_web_mock.dart'
if (dart.library.io) 'package:sentry_flutter/src/native/native_memory.dart';

void main() {
final testSrcList = Uint8List.fromList([1, 2, 3]);
final testSrcList = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
final testSrcData = testSrcList.buffer.asByteData();

test('empty list', () async {
final sut = NativeMemory.fromUint8List(Uint8List.fromList([]));
final sut = NativeMemory.fromByteData(ByteData(0));
expect(sut.length, 0);
expect(sut.pointer.address, greaterThan(0));
expect(sut.asTypedList(), isEmpty);
sut.free();
});

test('non-empty list', () async {
final sut = NativeMemory.fromUint8List(testSrcList);
expect(sut.length, 3);
final sut = NativeMemory.fromByteData(testSrcData);
expect(sut.length, 10);
expect(sut.pointer.address, greaterThan(0));
expect(sut.asTypedList(), testSrcList);
sut.free();
});

test('json', () async {
final sut = NativeMemory.fromUint8List(testSrcList);
final sut = NativeMemory.fromByteData(testSrcData);
final json = sut.toJson();
expect(json['address'], greaterThan(0));
expect(json['length'], 3);
expect(json['length'], 10);
expect(json.entries, hasLength(2));

final sut2 = NativeMemory.fromJson(json);
Expand Down
5 changes: 3 additions & 2 deletions flutter/test/native_memory_web_mock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ class NativeMemory {

const NativeMemory._(this.pointer, this.length);

factory NativeMemory.fromUint8List(Uint8List source) {
return NativeMemory._(Pointer<Uint8>._store(source), source.length);
factory NativeMemory.fromByteData(ByteData source) {
return NativeMemory._(Pointer<Uint8>._store(source.buffer.asUint8List()),
source.lengthInBytes);
}

factory NativeMemory.fromJson(Map<dynamic, dynamic> json) {
Expand Down
2 changes: 2 additions & 0 deletions flutter/test/replay/replay_native_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ void main() {

expect(json['length'], greaterThan(3000));
expect(json['address'], greaterThan(0));
expect(json['width'], greaterThan(0));
expect(json['height'], greaterThan(0));
NativeMemory.fromJson(json).free();
}

Expand Down
Loading