Skip to content

Commit

Permalink
merge sentry-native debug image creation to dart
Browse files Browse the repository at this point in the history
  • Loading branch information
vaind committed Sep 18, 2024
1 parent fd2d073 commit f9a3b73
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 129 deletions.
75 changes: 42 additions & 33 deletions dart/lib/src/load_dart_debug_images_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import '../sentry.dart';
class LoadDartDebugImagesIntegration extends Integration<SentryOptions> {
@override
void call(Hub hub, SentryOptions options) {
options.addEventProcessor(_LoadImageIntegrationEventProcessor(options));
options.sdk.addIntegration('loadDartImageIntegration');
if (options.enableDartSymbolication) {
options.addEventProcessor(LoadImageIntegrationEventProcessor(options));
options.sdk.addIntegration('loadDartImageIntegration');
}
}
}

class _LoadImageIntegrationEventProcessor implements EventProcessor {
_LoadImageIntegrationEventProcessor(this._options);
@internal
class LoadImageIntegrationEventProcessor implements EventProcessor {
LoadImageIntegrationEventProcessor(this._options);

final SentryOptions _options;

Expand All @@ -22,40 +25,42 @@ class _LoadImageIntegrationEventProcessor implements EventProcessor {

@override
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
if (_options.enableDartSymbolication) {
final stackTrace = event.stacktrace;
if (stackTrace!.frames.any((f) => f.platform == 'native')) {
try {
_debugImage ??= createDebugImage(stackTrace);
if (_debugImage != null) {
late final DebugMeta debugMeta;
if (event.debugMeta != null) {
final images = List<DebugImage>.from(event.debugMeta!.images);
images.add(_debugImage!);
debugMeta = event.debugMeta!.copyWith(images: images);
} else {
debugMeta = DebugMeta(images: [_debugImage!]);
}
return event.copyWith(debugMeta: debugMeta);
}
} catch (e, stack) {
_options.logger(
SentryLevel.info,
"Couldn't add Dart debug image to event. "
'The event will still be reported.',
exception: e,
stackTrace: stack,
);
if (_options.automatedTestMode) {
rethrow;
}
final stackTrace = event.stacktrace;
if (stackTrace!.frames.any((f) => f.platform == 'native')) {
final debugImage = getAppDebugImage(stackTrace);
if (debugImage != null) {
late final DebugMeta debugMeta;
if (event.debugMeta != null) {
final images = List<DebugImage>.from(event.debugMeta!.images);
images.add(debugImage);
debugMeta = event.debugMeta!.copyWith(images: images);
} else {
debugMeta = DebugMeta(images: [debugImage]);
}
return event.copyWith(debugMeta: debugMeta);
}
}

return event;
}

DebugImage? getAppDebugImage(SentryStackTrace stackTrace) {
try {
_debugImage ??= createDebugImage(stackTrace);
} catch (e, stack) {
_options.logger(
SentryLevel.info,
"Couldn't add Dart debug image to event. The event will still be reported.",
exception: e,
stackTrace: stack,
);
if (_options.automatedTestMode) {
rethrow;
}
}
return _debugImage;
}

@visibleForTesting
DebugImage? createDebugImage(SentryStackTrace stackTrace) {
if (stackTrace.buildId == null || stackTrace.baseAddr == null) {
Expand All @@ -74,10 +79,14 @@ class _LoadImageIntegrationEventProcessor implements EventProcessor {

final platform = _options.platformChecker.platform;

if (platform.isAndroid) {
if (platform.isAndroid || platform.isWindows) {
type = 'elf';
debugId = _convertBuildIdToDebugId(stackTrace.buildId!, platform.endian);
codeFile = 'libapp.so';
if (platform.isAndroid) {
codeFile = 'libapp.so';
} else if (platform.isWindows) {
codeFile = 'data/app.so';
}
} else if (platform.isIOS || platform.isMacOS) {
type = 'macho';
debugId = _formatHexToUuid(stackTrace.buildId!);
Expand Down
32 changes: 27 additions & 5 deletions dart/test/load_dart_debug_images_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ void main() {
MockPlatform.iOS(),
MockPlatform.macOS(),
MockPlatform.android(),
MockPlatform.windows(),
];

for (final platform in platforms) {
Expand All @@ -40,7 +41,7 @@ void main() {
expect(fixture.options.eventProcessors.length, 1);
expect(
fixture.options.eventProcessors.first.runtimeType.toString(),
'_LoadImageIntegrationEventProcessor',
'LoadImageIntegrationEventProcessor',
);
});

Expand Down Expand Up @@ -98,10 +99,10 @@ isolate_dso_base: 20000000
#00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7
''');

if (platform.isAndroid) {
expect(debugImage?.debugId, '89cb80b6-9e0f-123c-a24b-172d050dec73');
} else {
if (platform.isIOS || platform.isMacOS) {
expect(debugImage?.debugId, 'b680cb89-0f9e-3c12-a24b-172d050dec73');
} else {
expect(debugImage?.debugId, '89cb80b6-9e0f-123c-a24b-172d050dec73');
}
});

Expand All @@ -125,10 +126,31 @@ isolate_dso_base: 40000000
#00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7
''');

if (platform.isAndroid) {
if (platform.isAndroid || platform.isWindows) {
expect(debugImage?.type, 'elf');
} else if (platform.isIOS || platform.isMacOS) {
expect(debugImage?.type, 'macho');
} else {
fail('missing case for platform $platform');
}
});

test('sets codeFile based on platform', () async {
final debugImage = await fixture.parseAndProcess('''
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
build_id: 'b680cb890f9e3c12a24b172d050dec73'
isolate_dso_base: 40000000
#00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7
''');

if (platform.isAndroid) {
expect(debugImage?.codeFile, 'libapp.so');
} else if (platform.isWindows) {
expect(debugImage?.codeFile, 'data/app.so');
} else if (platform.isIOS || platform.isMacOS) {
expect(debugImage?.codeFile, 'App.Framework/App');
} else {
fail('missing case for platform $platform');
}
});

Expand Down
22 changes: 19 additions & 3 deletions flutter/lib/src/integrations/load_image_list_integration.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'dart:async';

import 'package:sentry/sentry.dart';
// ignore: implementation_imports
import 'package:sentry/src/load_dart_debug_images_integration.dart';

import '../native/sentry_native_binding.dart';
import '../sentry_flutter_options.dart';

Expand All @@ -14,18 +17,21 @@ class LoadImageListIntegration extends Integration<SentryFlutterOptions> {
@override
void call(Hub hub, SentryFlutterOptions options) {
options.addEventProcessor(
_LoadImageListIntegrationEventProcessor(_native),
_LoadImageListIntegrationEventProcessor(options, _native),
);

options.sdk.addIntegration('loadImageListIntegration');
}
}

class _LoadImageListIntegrationEventProcessor implements EventProcessor {
_LoadImageListIntegrationEventProcessor(this._native);
_LoadImageListIntegrationEventProcessor(this._options, this._native);

final SentryFlutterOptions _options;
final SentryNativeBinding _native;

late final _dartProcessor = LoadImageIntegrationEventProcessor(_options);

@override
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
// ignore: invalid_use_of_internal_member
Expand All @@ -34,7 +40,17 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor {
// if the stacktrace has native frames, we load native debug images.
if (stackTrace != null &&
stackTrace.frames.any((frame) => 'native' == frame.platform)) {
final images = await _native.loadDebugImages(stackTrace);
var images = await _native.loadDebugImages(stackTrace);

// On windows, we need to add the ELF debug image of the AOT code.
// See https://github.com/flutter/flutter/issues/154840
if (_options.platformChecker.platform.isWindows) {
final debugImage = _dartProcessor.getAppDebugImage(stackTrace);

Check warning on line 48 in flutter/lib/src/integrations/load_image_list_integration.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/integrations/load_image_list_integration.dart#L48

Added line #L48 was not covered by tests
if (debugImage != null) {
images ??= List.empty();
images.add(debugImage);

Check warning on line 51 in flutter/lib/src/integrations/load_image_list_integration.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/integrations/load_image_list_integration.dart#L50-L51

Added lines #L50 - L51 were not covered by tests
}
}
if (images != null) {
return event.copyWith(debugMeta: DebugMeta(images: images));
}
Expand Down
87 changes: 0 additions & 87 deletions flutter/lib/src/native/c/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:ffi/ffi.dart';
import 'package:meta/meta.dart';

Expand All @@ -16,7 +15,6 @@ import 'utils.dart';

@internal
class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding {
DebugImage? _appDebugImage;
@override
final SentryFlutterOptions options;

Expand Down Expand Up @@ -244,97 +242,12 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding {
codeId: cImage.get('code_id').castPrimitive(options.logger),

Check warning on line 242 in flutter/lib/src/native/c/sentry_native.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/c/sentry_native.dart#L232-L242

Added lines #L232 - L242 were not covered by tests
);
});

// On windows, we need to add the ELF debug image of the AOT code.
// See https://github.com/flutter/flutter/issues/154840
if (options.platformChecker.platform.isWindows) {
_appDebugImage ??= await getAppDebugImage(stackTrace, images);
if (_appDebugImage != null) {
images.add(_appDebugImage!);
}
}

return images;
} finally {
native.value_decref(cImages);

Check warning on line 247 in flutter/lib/src/native/c/sentry_native.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/c/sentry_native.dart#L247

Added line #L247 was not covered by tests
}
});

@visibleForTesting
Future<DebugImage?> getAppDebugImage(
SentryStackTrace stackTrace, Iterable<DebugImage> nativeImages) async {
// ignore: invalid_use_of_internal_member
final buildId = stackTrace.buildId;
// ignore: invalid_use_of_internal_member
final imageAddr = stackTrace.baseAddr;

if (buildId == null || imageAddr == null) {
return null;
}

final exePath = nativeImages
.firstWhereOrNull(
(image) => image.codeFile?.toLowerCase().endsWith('.exe') ?? false)
?.codeFile;
if (exePath == null) {
options.logger(
SentryLevel.debug,
"Couldn't add AOT ELF image for server-side symbolication because the "
"app executable is not among the debug images reported by native.");
return null;
}

final appSoFile = options.fileSystem
.file(exePath)
.parent
.childDirectory('data')
.childFile('app.so');
if (!await appSoFile.exists()) {
options.logger(SentryLevel.debug,
"Couldn't add AOT ELF image because ${appSoFile.path} doesn't exist.");
return null;
}

final stat = await appSoFile.stat();
return DebugImage(
type: 'elf',
imageAddr: imageAddr,
imageSize: stat.size,
codeFile: appSoFile.path,
codeId: buildId,
debugId: _computeDebugId(buildId),
);
}

/// See https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734
/// Converts an ELF object identifier into a `DebugId`.
///
/// The identifier data is first truncated or extended to match 16 byte size of
/// Uuids. If the data is declared in little endian, the first three Uuid fields
/// are flipped to match the big endian expected by the breakpad processor.
///
/// The `DebugId::appendix` field is always `0` for ELF.
String? _computeDebugId(String buildId) {
// Make sure that we have exactly UUID_SIZE bytes available
const uuidSize = 16 * 2;
final data = Uint8List(uuidSize);
final len = buildId.length.clamp(0, uuidSize);
data.setAll(0, buildId.codeUnits.take(len));

if (Endian.host == Endian.little) {
// The file ELF file targets a little endian architecture. Convert to
// network byte order (big endian) to match the Breakpad processor's
// expectations. For big endian object files, this is not needed.
// To manipulate this as hex, we create an Uint16 view.
final data16 = Uint16List.view(data.buffer);
data16.setRange(0, 4, data16.sublist(0, 4).reversed);
data16.setRange(4, 6, data16.sublist(4, 6).reversed);
data16.setRange(6, 8, data16.sublist(6, 8).reversed);
}

return String.fromCharCodes(data);
}

@override

Check warning on line 251 in flutter/lib/src/native/c/sentry_native.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/c/sentry_native.dart#L251

Added line #L251 was not covered by tests
FutureOr<void> pauseAppHangTracking() {}

Expand Down
2 changes: 1 addition & 1 deletion flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ dependencies:
meta: ^1.3.0
ffi: ^2.0.0
file: '>=6.1.4'
collection: ^1.16.0

dev_dependencies:
build_runner: ^2.4.2
Expand All @@ -37,6 +36,7 @@ dev_dependencies:
mockito: ^5.1.0
yaml: ^3.1.0 # needed for version match (code and pubspec)
flutter_lints: ^4.0.0
collection: ^1.16.0
remove_from_coverage: ^2.0.0
flutter_localizations:
sdk: flutter
Expand Down

0 comments on commit f9a3b73

Please sign in to comment.