From 3c54d0666daec595da141359f2a5cbdeae4c5f93 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 20 Dec 2024 17:59:19 +0100 Subject: [PATCH 1/8] iOS copy native replay screenshot in-memory to native --- .../SentryFlutterReplayScreenshotProvider.m | 25 ++++--- .../src/native/cocoa/sentry_native_cocoa.dart | 8 ++- flutter/lib/src/native/native_memory.dart | 47 +++++++++++++ flutter/test/mocks.dart | 15 +++-- flutter/test/native_memory_test.dart | 44 +++++++++++++ flutter/test/replay/replay_native_test.dart | 66 +++++++++++-------- 6 files changed, 162 insertions(+), 43 deletions(-) create mode 100644 flutter/lib/src/native/native_memory.dart create mode 100644 flutter/test/native_memory_test.dart diff --git a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m index b363cf5342..20da1b8b31 100644 --- a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m +++ b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m @@ -26,26 +26,31 @@ - (void)imageWithView:(UIView *_Nonnull)view invokeMethod:@"captureReplayScreenshot" arguments:@{@"replayId" : replayId ? replayId : [NSNull null]} result:^(id value) { - if (value == nil) { + if (value == nil || value == 0) { NSLog(@"SentryFlutterReplayScreenshotProvider received null " @"result. " @"Cannot capture a replay screenshot."); - } else if ([value - isKindOfClass:[FlutterStandardTypedData class]]) { - FlutterStandardTypedData *typedData = - (FlutterStandardTypedData *)value; - UIImage *image = [UIImage imageWithData:typedData.data]; + } else if ([value isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = (NSDictionary *)value; + long address = ((NSNumber *)dict[@"address"]).longValue; + unsigned long length = + ((NSNumber *)dict[@"length"]).unsignedLongValue; + NSData *data = [NSData dataWithBytesNoCopy:(void *)address + length:length + freeWhenDone:TRUE]; + UIImage *image = [UIImage imageWithData:data]; onComplete(image); + return; } else if ([value isKindOfClass:[FlutterError class]]) { FlutterError *error = (FlutterError *)value; NSLog(@"SentryFlutterReplayScreenshotProvider received an " @"error: %@. Cannot capture a replay screenshot.", error.message); - } else { - NSLog(@"SentryFlutterReplayScreenshotProvider received an " - @"unexpected result. " - @"Cannot capture a replay screenshot."); + return; } + NSLog(@"SentryFlutterReplayScreenshotProvider received an " + @"unexpected result. " + @"Cannot capture a replay screenshot."); }]; } diff --git a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 5a89d6fb40..8803ed6f9d 100644 --- a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -10,6 +10,7 @@ import '../../replay/replay_config.dart'; import '../../replay/replay_recorder.dart'; import '../../screenshot/recorder.dart'; import '../../screenshot/recorder_config.dart'; +import '../native_memory.dart'; import '../sentry_native_channel.dart'; import 'binding.dart' as cocoa; @@ -35,7 +36,7 @@ class SentryNativeCocoa extends SentryNativeChannel { _replayRecorder ??= ReplayScreenshotRecorder(ScreenshotRecorderConfig(), options); - final replayId = call.arguments['replayId'] == null + final replayId = call.arguments ['replayId'] == null ? null : SentryId.fromId(call.arguments['replayId'] as String); if (_replayId != replayId) { @@ -73,7 +74,10 @@ class SentryNativeCocoa extends SentryNativeChannel { } }).then(completer.complete, onError: completer.completeError); }); - return completer.future; + final uint8List = await completer.future; + + //Malloc memory and copy the data. Native must free it. + return uint8List?.toNativeMemory().toJson(); default: throw UnimplementedError('Method ${call.method} not implemented'); } diff --git a/flutter/lib/src/native/native_memory.dart b/flutter/lib/src/native/native_memory.dart new file mode 100644 index 0000000000..8dd161a0a2 --- /dev/null +++ b/flutter/lib/src/native/native_memory.dart @@ -0,0 +1,47 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import 'package:ffi/ffi.dart' as pkg_ffi; + +@internal +@immutable +class NativeMemory { + final Pointer pointer; + final int length; + + const NativeMemory._(this.pointer, this.length); + + factory NativeMemory.fromUint8List(Uint8List source) { + final length = source.length; + final ptr = pkg_ffi.malloc.allocate(length); + if (length > 0) { + ptr.asTypedList(length).setAll(0, source); + } + return NativeMemory._(ptr, length); + } + + factory NativeMemory.fromJson(Map json) { + final length = json['length'] as int; + final ptr = Pointer.fromAddress(json['address'] as int); + return NativeMemory._(ptr, length); + } + + /// Frees the underlying native memory. + /// You must not use this object after freeing. + void free() { + pkg_ffi.malloc.free(pointer); + } + + Uint8List asTypedList() => pointer.asTypedList(length); + + Map toJson() => { + 'address': pointer.address, + 'length': length, + }; +} + +@internal +extension Uint8ListNativeMemory on Uint8List { + NativeMemory toNativeMemory() => NativeMemory.fromUint8List(this); +} diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index 66e05b68eb..b13467e7c1 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -202,10 +202,11 @@ class NativeChannelFixture { handler; static TestDefaultBinaryMessenger get _messenger => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + late final codec = StandardMethodCodec(); NativeChannelFixture() { TestWidgetsFlutterBinding.ensureInitialized(); - channel = MethodChannel('test.channel', StandardMethodCodec(), _messenger); + channel = MethodChannel('test.channel', codec, _messenger); handler = MockCallbacks().methodCallHandler; when(handler('initNativeSdk', any)).thenAnswer((_) => Future.value()); when(handler('closeNativeSdk', any)).thenAnswer((_) => Future.value()); @@ -214,11 +215,15 @@ class NativeChannelFixture { } // Mock this call as if it was invoked by the native side. - Future invokeFromNative(String method, [dynamic arguments]) async { - final call = - StandardMethodCodec().encodeMethodCall(MethodCall(method, arguments)); - return _messenger.handlePlatformMessage( + Future invokeFromNative(String method, [dynamic arguments]) async { + final call = codec.encodeMethodCall(MethodCall(method, arguments)); + final byteData = await _messenger.handlePlatformMessage( channel.name, call, (ByteData? data) {}); + if (byteData != null) { + return codec.decodeEnvelope(byteData); + } else { + return null; + } } } diff --git a/flutter/test/native_memory_test.dart b/flutter/test/native_memory_test.dart new file mode 100644 index 0000000000..ec5f3838e8 --- /dev/null +++ b/flutter/test/native_memory_test.dart @@ -0,0 +1,44 @@ +@TestOn('vm') +library flutter_test; + +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/src/native/native_memory.dart'; + +void main() { + final testSrcList = Uint8List.fromList([1, 2, 3]); + + test('empty list', () async { + final sut = NativeMemory.fromUint8List(Uint8List.fromList([])); + expect(sut.length, 0); + expect(sut.pointer, isNot(nullptr)); + expect(sut.asTypedList(), isEmpty); + sut.free(); + }); + + test('non-empty list', () async { + final sut = NativeMemory.fromUint8List(testSrcList); + expect(sut.length, 3); + expect(sut.pointer, isNot(nullptr)); + expect(sut.asTypedList(), testSrcList); + sut.free(); + }); + + test('json', () async { + final sut = NativeMemory.fromUint8List(testSrcList); + final json = sut.toJson(); + expect(json['address'], greaterThan(0)); + expect(json['length'], 3); + expect(json.entries, hasLength(2)); + + final sut2 = NativeMemory.fromJson(json); + expect(sut2.toJson(), json); + expect(sut2.asTypedList(), testSrcList); + + expect(sut.pointer, sut2.pointer); + expect(sut.length, sut2.length); + sut2.free(); + }); +} diff --git a/flutter/test/replay/replay_native_test.dart b/flutter/test/replay/replay_native_test.dart index d40402c00f..7ded7ae031 100644 --- a/flutter/test/replay/replay_native_test.dart +++ b/flutter/test/replay/replay_native_test.dart @@ -11,6 +11,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/factory.dart'; +import 'package:sentry_flutter/src/native/native_memory.dart'; import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; import '../mocks.dart'; @@ -76,29 +77,35 @@ void main() { await sut.init(hub); }); - test('sets replay ID to context', () async { - // verify there was no scope configured before - verifyNever(hub.configureScope(any)); - - // emulate the native platform invoking the method - await native.invokeFromNative( - mockPlatform.isAndroid - ? 'ReplayRecorder.start' - : 'captureReplayScreenshot', - replayConfig); + testWidgets('sets replayID to context', (tester) async { + await tester.runAsync(() async { + // verify there was no scope configured before + verifyNever(hub.configureScope(any)); + when(hub.configureScope(captureAny)).thenReturn(null); - // verify the replay ID was set - final closure = - verify(hub.configureScope(captureAny)).captured.single; - final scope = Scope(options); - expect(scope.replayId, isNull); - await closure(scope); - expect(scope.replayId.toString(), replayConfig['replayId']); + // emulate the native platform invoking the method + final future = native.invokeFromNative( + mockPlatform.isAndroid + ? 'ReplayRecorder.start' + : 'captureReplayScreenshot', + replayConfig); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await future; + + // verify the replay ID was set + final closure = + verify(hub.configureScope(captureAny)).captured.single; + final scope = Scope(options); + expect(scope.replayId, isNull); + await closure(scope); + expect(scope.replayId.toString(), replayConfig['replayId']); + }); }); test('clears replay ID from context', () async { // verify there was no scope configured before verifyNever(hub.configureScope(any)); + when(hub.configureScope(captureAny)).thenReturn(null); // emulate the native platform invoking the method await native.invokeFromNative('ReplayRecorder.stop'); @@ -116,6 +123,7 @@ void main() { testWidgets('captures images', (tester) async { await tester.runAsync(() async { when(hub.configureScope(captureAny)).thenReturn(null); + await pumpTestElement(tester); pumpAndSettle() => tester.pumpAndSettle(const Duration(seconds: 1)); @@ -198,17 +206,23 @@ void main() { expect(capturedImages, equals(fsImages())); expect(capturedImages.length, count); } else if (mockPlatform.isIOS) { - var imagaData = native.invokeFromNative( - 'captureReplayScreenshot', replayConfig); - await pumpAndSettle(); - expect((await imagaData)?.lengthInBytes, greaterThan(3000)); + Future captureAndVerify() async { + final future = native.invokeFromNative( + 'captureReplayScreenshot', replayConfig); + await pumpAndSettle(); + final json = (await future) as Map; + + expect(json['length'], greaterThan(3000)); + expect(json['address'], greaterThan(0)); + NativeMemory.fromJson(json).free(); + } + + await captureAndVerify(); - // Happens if the session-replay rate is 0. + // Check everything works if session-replay rate is 0, + // which causes replayId to be 0 as well. replayConfig['replayId'] = null; - imagaData = native.invokeFromNative( - 'captureReplayScreenshot', replayConfig); - await pumpAndSettle(); - expect((await imagaData)?.lengthInBytes, greaterThan(3000)); + await captureAndVerify(); } else { fail('unsupported platform'); } From 0bebd52776406de7deffc95570d58cb2dcca1fb2 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 20 Dec 2024 19:33:29 +0100 Subject: [PATCH 2/8] formatting --- flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 8803ed6f9d..9b9ce4f7e8 100644 --- a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -36,7 +36,7 @@ class SentryNativeCocoa extends SentryNativeChannel { _replayRecorder ??= ReplayScreenshotRecorder(ScreenshotRecorderConfig(), options); - final replayId = call.arguments ['replayId'] == null + final replayId = call.arguments['replayId'] == null ? null : SentryId.fromId(call.arguments['replayId'] as String); if (_replayId != replayId) { From e420459759c682ff79d1ed22d25cf2bc5e2efcc6 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 20 Dec 2024 20:31:07 +0100 Subject: [PATCH 3/8] fix tests on web --- flutter/test/native_memory_test.dart | 8 +-- flutter/test/native_memory_web_mock.dart | 60 +++++++++++++++++++++ flutter/test/replay/replay_native_test.dart | 3 +- 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 flutter/test/native_memory_web_mock.dart diff --git a/flutter/test/native_memory_test.dart b/flutter/test/native_memory_test.dart index ec5f3838e8..9e49feb8ca 100644 --- a/flutter/test/native_memory_test.dart +++ b/flutter/test/native_memory_test.dart @@ -1,11 +1,11 @@ @TestOn('vm') library flutter_test; -import 'dart:ffi'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/src/native/native_memory.dart'; +import 'package:sentry_flutter/src/native/native_memory.dart' + if (dart.library.html) 'native_memory_web_mock.dart'; void main() { final testSrcList = Uint8List.fromList([1, 2, 3]); @@ -13,7 +13,7 @@ void main() { test('empty list', () async { final sut = NativeMemory.fromUint8List(Uint8List.fromList([])); expect(sut.length, 0); - expect(sut.pointer, isNot(nullptr)); + expect(sut.pointer.address, greaterThan(0)); expect(sut.asTypedList(), isEmpty); sut.free(); }); @@ -21,7 +21,7 @@ void main() { test('non-empty list', () async { final sut = NativeMemory.fromUint8List(testSrcList); expect(sut.length, 3); - expect(sut.pointer, isNot(nullptr)); + expect(sut.pointer.address, greaterThan(0)); expect(sut.asTypedList(), testSrcList); sut.free(); }); diff --git a/flutter/test/native_memory_web_mock.dart b/flutter/test/native_memory_web_mock.dart new file mode 100644 index 0000000000..81e03fee6f --- /dev/null +++ b/flutter/test/native_memory_web_mock.dart @@ -0,0 +1,60 @@ +import 'dart:math'; +import 'dart:typed_data'; + +// This is just a mock so `flutter test --platform chrome` works. +// See https://github.com/flutter/flutter/issues/160675 +class NativeMemory { + final Pointer pointer; + final int length; + + const NativeMemory._(this.pointer, this.length); + + factory NativeMemory.fromUint8List(Uint8List source) { + return NativeMemory._(Pointer._store(source), source.length); + } + + factory NativeMemory.fromJson(Map json) { + return NativeMemory._( + Pointer._load(json['address'] as int), json['length'] as int); + } + + void free() {} + + Uint8List asTypedList() => _memory[pointer.address]!; + + Map toJson() => { + 'address': pointer.address, + 'length': length, + }; +} + +class Pointer { + final int address; + + const Pointer(this.address); + + factory Pointer._store(Uint8List data) { + final address = Random().nextInt(999999); + _memory[address] = data; + return Pointer(address); + } + + factory Pointer._load(int address) { + return Pointer(address); + } + + /// Equality for Pointers only depends on their address. + @override + bool operator ==(Object other) { + if (other is! Pointer) return false; + return address == other.address; + } + + /// The hash code for a Pointer only depends on its address. + @override + int get hashCode => address.hashCode; +} + +class Uint8 {} + +final _memory = {}; diff --git a/flutter/test/replay/replay_native_test.dart b/flutter/test/replay/replay_native_test.dart index 7ded7ae031..b4c0e65479 100644 --- a/flutter/test/replay/replay_native_test.dart +++ b/flutter/test/replay/replay_native_test.dart @@ -11,7 +11,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/factory.dart'; -import 'package:sentry_flutter/src/native/native_memory.dart'; +import 'package:sentry_flutter/src/native/native_memory.dart' + if (dart.library.html) '../native_memory_web_mock.dart'; import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; import '../mocks.dart'; From 0db8eb3ae062039f83237f1b3fc94b9329eeeb4d Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 20 Dec 2024 20:39:41 +0100 Subject: [PATCH 4/8] chore: changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 912eed0243..0e84dc3e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Enahncements + +- Replay: improve performance of screenshot data to native recorder ([#2530](https://github.com/getsentry/sentry-dart/pull/2530)) + ## 8.12.0-beta.2 ### Deprecations From 4893a36261f46c06748ccfeb900b2dfd6f692efa Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 20 Dec 2024 23:12:42 +0100 Subject: [PATCH 5/8] cleanups --- CHANGELOG.md | 2 +- .../SentryFlutterReplayScreenshotProvider.m | 19 ++++++++++--------- .../src/native/cocoa/sentry_native_cocoa.dart | 2 +- flutter/lib/src/native/native_memory.dart | 4 +--- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e84dc3e16..5e8044ab65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -### Enahncements +### Enhancements - Replay: improve performance of screenshot data to native recorder ([#2530](https://github.com/getsentry/sentry-dart/pull/2530)) diff --git a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m index 20da1b8b31..44c566ce91 100644 --- a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m +++ b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m @@ -26,18 +26,18 @@ - (void)imageWithView:(UIView *_Nonnull)view invokeMethod:@"captureReplayScreenshot" arguments:@{@"replayId" : replayId ? replayId : [NSNull null]} result:^(id value) { - if (value == nil || value == 0) { + if (value == nil) { NSLog(@"SentryFlutterReplayScreenshotProvider received null " @"result. " @"Cannot capture a replay screenshot."); } else if ([value isKindOfClass:[NSDictionary class]]) { NSDictionary *dict = (NSDictionary *)value; long address = ((NSNumber *)dict[@"address"]).longValue; - unsigned long length = - ((NSNumber *)dict[@"length"]).unsignedLongValue; - NSData *data = [NSData dataWithBytesNoCopy:(void *)address - length:length - freeWhenDone:TRUE]; + (NSNumber *)length = ((NSNumber *)dict[@"length"]); + NSData *data = + [NSData dataWithBytesNoCopy:(void *)address + length:length.unsignedLongValue + freeWhenDone:TRUE]; UIImage *image = [UIImage imageWithData:data]; onComplete(image); return; @@ -47,10 +47,11 @@ - (void)imageWithView:(UIView *_Nonnull)view @"error: %@. Cannot capture a replay screenshot.", error.message); return; + } else { + NSLog(@"SentryFlutterReplayScreenshotProvider received an " + @"unexpected result. " + @"Cannot capture a replay screenshot."); } - NSLog(@"SentryFlutterReplayScreenshotProvider received an " - @"unexpected result. " - @"Cannot capture a replay screenshot."); }]; } diff --git a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 9b9ce4f7e8..621d8f8a40 100644 --- a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -76,7 +76,7 @@ class SentryNativeCocoa extends SentryNativeChannel { }); final uint8List = await completer.future; - //Malloc memory and copy the data. Native must free it. + // Malloc memory and copy the data. Native must free it. return uint8List?.toNativeMemory().toJson(); default: throw UnimplementedError('Method ${call.method} not implemented'); diff --git a/flutter/lib/src/native/native_memory.dart b/flutter/lib/src/native/native_memory.dart index 8dd161a0a2..daf8458d02 100644 --- a/flutter/lib/src/native/native_memory.dart +++ b/flutter/lib/src/native/native_memory.dart @@ -29,9 +29,7 @@ class NativeMemory { /// Frees the underlying native memory. /// You must not use this object after freeing. - void free() { - pkg_ffi.malloc.free(pointer); - } + void free() => pkg_ffi.malloc.free(pointer); Uint8List asTypedList() => pointer.asTypedList(length); From f485ade7689699e524520f2e00ac570c237bf53b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 20 Dec 2024 23:12:49 +0100 Subject: [PATCH 6/8] fix wasm test --- flutter/test/native_memory_test.dart | 4 ++-- flutter/test/replay/replay_native_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/test/native_memory_test.dart b/flutter/test/native_memory_test.dart index 9e49feb8ca..055ccf90f7 100644 --- a/flutter/test/native_memory_test.dart +++ b/flutter/test/native_memory_test.dart @@ -4,8 +4,8 @@ library flutter_test; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/src/native/native_memory.dart' - if (dart.library.html) 'native_memory_web_mock.dart'; +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]); diff --git a/flutter/test/replay/replay_native_test.dart b/flutter/test/replay/replay_native_test.dart index b4c0e65479..424da6706c 100644 --- a/flutter/test/replay/replay_native_test.dart +++ b/flutter/test/replay/replay_native_test.dart @@ -11,8 +11,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/factory.dart'; -import 'package:sentry_flutter/src/native/native_memory.dart' - if (dart.library.html) '../native_memory_web_mock.dart'; +import '../native_memory_web_mock.dart' + if (dart.library.io) 'package:sentry_flutter/src/native/native_memory.dart'; import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; import '../mocks.dart'; From cd51c10c442c95ddecdd6eb22ee75f262376236c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Sat, 21 Dec 2024 12:34:35 +0100 Subject: [PATCH 7/8] objc syntax error --- flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m index 44c566ce91..61d9e87e54 100644 --- a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m +++ b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m @@ -33,7 +33,7 @@ - (void)imageWithView:(UIView *_Nonnull)view } else if ([value isKindOfClass:[NSDictionary class]]) { NSDictionary *dict = (NSDictionary *)value; long address = ((NSNumber *)dict[@"address"]).longValue; - (NSNumber *)length = ((NSNumber *)dict[@"length"]); + NSNumber *length = ((NSNumber *)dict[@"length"]); NSData *data = [NSData dataWithBytesNoCopy:(void *)address length:length.unsignedLongValue From 6c0fec08ae3650c04ee14c3bc857bfbbb632a934 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 23 Dec 2024 13:48:43 +0100 Subject: [PATCH 8/8] comment --- flutter/lib/src/native/native_memory.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flutter/lib/src/native/native_memory.dart b/flutter/lib/src/native/native_memory.dart index daf8458d02..a7c7be0008 100644 --- a/flutter/lib/src/native/native_memory.dart +++ b/flutter/lib/src/native/native_memory.dart @@ -29,6 +29,10 @@ class NativeMemory { /// Frees the underlying native memory. /// You must not use this object after freeing. + /// + /// Currently, we only need to do this in tests because there's no native + /// counterpart to free the memory. + @visibleForTesting void free() => pkg_ffi.malloc.free(pointer); Uint8List asTypedList() => pointer.asTypedList(length);