diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce461cd63..dbd6ac0943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Tag all spans with thread info ([#3101](https://github.com/getsentry/sentry-dart/pull/3101)) +### Enhancements + +- Improve envelope conversion to `Uint8List` in `FileSystemTransport` ([#3147](https://github.com/getsentry/sentry-dart/pull/3147)) + ## 9.6.0 Note: this release might require updating your Android Gradle Plugin version to at least `8.1.4`. diff --git a/packages/flutter/lib/src/file_system_transport.dart b/packages/flutter/lib/src/file_system_transport.dart index 7a43b49af0..102ead21e2 100644 --- a/packages/flutter/lib/src/file_system_transport.dart +++ b/packages/flutter/lib/src/file_system_transport.dart @@ -1,9 +1,5 @@ -// backcompatibility for Flutter < 3.3 -// ignore: unnecessary_import import 'dart:typed_data'; -import 'package:flutter/services.dart'; - import '../sentry_flutter.dart'; import 'native/sentry_native_binding.dart'; @@ -15,12 +11,13 @@ class FileSystemTransport implements Transport { @override Future send(SentryEnvelope envelope) async { - final envelopeData = []; - await envelope.envelopeStream(_options).forEach(envelopeData.addAll); + final bytesBuilder = BytesBuilder(copy: false); + await envelope.envelopeStream(_options).forEach(bytesBuilder.add); + final envelopeData = bytesBuilder.takeBytes(); + try { - // TODO avoid copy - await _native.captureEnvelope(Uint8List.fromList(envelopeData), - envelope.containsUnhandledException); + await _native.captureEnvelope( + envelopeData, envelope.containsUnhandledException); } catch (exception, stackTrace) { _options.log( SentryLevel.error, diff --git a/packages/flutter/microbenchmarks/lib/main.dart b/packages/flutter/microbenchmarks/lib/main.dart index 27471fb732..a96f9b246f 100644 --- a/packages/flutter/microbenchmarks/lib/main.dart +++ b/packages/flutter/microbenchmarks/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'src/image_bench.dart' as image_bench; import 'src/memory_bench.dart' as memory_bench; import 'src/jni_bench.dart' as jni_bench; +import 'src/envelope_builder_bench.dart' as envelope_builder_bench; typedef BenchmarkSet = (String name, Future Function() callback); @@ -12,6 +13,7 @@ Future main() async { ('Image', image_bench.execute), ('Memory', memory_bench.execute), if (Platform.isAndroid) ('JNI', jni_bench.execute), + ('Envelope builder', envelope_builder_bench.execute) ]; RegExp? filterRegexp; diff --git a/packages/flutter/microbenchmarks/lib/src/envelope_builder_bench.dart b/packages/flutter/microbenchmarks/lib/src/envelope_builder_bench.dart new file mode 100644 index 0000000000..daa3fc4608 --- /dev/null +++ b/packages/flutter/microbenchmarks/lib/src/envelope_builder_bench.dart @@ -0,0 +1,174 @@ +import 'dart:typed_data'; +import 'dart:math'; + +const _minIterations = 50; +const _maxIterations = 1000; + +Future execute() async { + print('Envelope Builder Benchmark'); + print('=========================='); + print('Comparing legacy List vs BytesBuilder approaches\n'); + + // Test with different envelope sizes + final sizes = [ + (1024, '1 KB'), + (10 * 1024, '10 KB'), + (100 * 1024, '100 KB'), + (1024 * 1024, '1 MB'), + (5 * 1024 * 1024, '5 MB'), + ]; + + for (final (size, label) in sizes) { + print('Envelope size: $label'); + print('-' * 40); + + // Use adaptive iteration count based on data size + final iterations = _getIterationCount(size); + print('Running $iterations iterations...'); + + // Create mock envelope data + final mockData = _generateMockEnvelopeData(size); + + // Benchmark legacy approach + final legacyResults = await _benchmarkLegacyApproach(mockData, iterations); + final legacyAvg = + legacyResults.reduce((a, b) => a + b) / legacyResults.length; + final legacyMin = legacyResults.reduce(min); + final legacyMax = legacyResults.reduce(max); + + // Benchmark new approach + final newResults = await _benchmarkNewApproach(mockData, iterations); + final newAvg = newResults.reduce((a, b) => a + b) / newResults.length; + final newMin = newResults.reduce(min); + final newMax = newResults.reduce(max); + + // Calculate improvement + final improvement = + ((legacyAvg - newAvg) / legacyAvg * 100).toStringAsFixed(1); + final speedup = (legacyAvg / newAvg).toStringAsFixed(2); + + print('Legacy approach (List + addAll):'); + print(' Average: ${_formatMicroseconds(legacyAvg)}'); + print(' Min: ${_formatMicroseconds(legacyMin)}'); + print(' Max: ${_formatMicroseconds(legacyMax)}'); + + print('New approach (BytesBuilder):'); + print(' Average: ${_formatMicroseconds(newAvg)}'); + print(' Min: ${_formatMicroseconds(newMin)}'); + print(' Max: ${_formatMicroseconds(newMax)}'); + + print('Performance improvement: $improvement% (${speedup}x faster)'); + print(''); + } +} + +// Adaptive iteration count to avoid memory pressure and hanging +int _getIterationCount(int dataSize) { + if (dataSize <= 10 * 1024) { + return _maxIterations; // 1K iterations for <= 10KB + } else if (dataSize <= 100 * 1024) { + return _maxIterations ~/ 2; // 500 iterations for <= 100KB + } else if (dataSize <= 1024 * 1024) { + return _maxIterations ~/ 5; // 200 iterations for <= 1MB + } else { + return _minIterations; // 50 iterations for > 1MB + } +} + +// Generate mock envelope data chunks to simulate streaming +List> _generateMockEnvelopeData(int totalSize) { + final chunks = >[]; + final random = Random(42); // Fixed seed for reproducibility + + // Simulate realistic chunk sizes (similar to how envelope streams work) + final chunkSizes = [64, 128, 256, 512, 1024]; + var remaining = totalSize; + + while (remaining > 0) { + final chunkSize = chunkSizes[random.nextInt(chunkSizes.length)]; + final actualSize = remaining < chunkSize ? remaining : chunkSize; + + // Create chunk with random data + final chunk = List.generate(actualSize, (_) => random.nextInt(256)); + chunks.add(chunk); + remaining -= actualSize; + } + + return chunks; +} + +Future> _benchmarkLegacyApproach( + List> chunks, int iterations) async { + final results = []; + + // Reduced warmup for large data + final warmupIterations = min(20, iterations ~/ 5); + + // Warmup + for (var i = 0; i < warmupIterations; i++) { + _runLegacyApproach(chunks); + } + + // Actual benchmark + for (var i = 0; i < iterations; i++) { + final stopwatch = Stopwatch()..start(); + _runLegacyApproach(chunks); + stopwatch.stop(); + results.add(stopwatch.elapsedMicroseconds.toDouble()); + } + + return results; +} + +Future> _benchmarkNewApproach( + List> chunks, int iterations) async { + final results = []; + + // Reduced warmup for large data + final warmupIterations = min(20, iterations ~/ 5); + + // Warmup + for (var i = 0; i < warmupIterations; i++) { + _runNewApproach(chunks); + } + + // Actual benchmark + for (var i = 0; i < iterations; i++) { + final stopwatch = Stopwatch()..start(); + _runNewApproach(chunks); + stopwatch.stop(); + results.add(stopwatch.elapsedMicroseconds.toDouble()); + } + + return results; +} + +Uint8List _runLegacyApproach(List> chunks) { + final envelopeData = []; + for (final chunk in chunks) { + envelopeData.addAll(chunk); + } + return Uint8List.fromList(envelopeData); +} + +Uint8List _runNewApproach(List> chunks) { + final builder = BytesBuilder(copy: false); + for (final chunk in chunks) { + builder.add(chunk); + } + return builder.takeBytes(); +} + +String _formatMicroseconds(double microseconds) { + if (microseconds < 1000) { + return '${microseconds.toStringAsFixed(1)} μs'; + } else if (microseconds < 1000000) { + return '${(microseconds / 1000).toStringAsFixed(2)} ms'; + } else { + return '${(microseconds / 1000000).toStringAsFixed(2)} s'; + } +} + +void main() async { + await execute(); +}