Skip to content

Commit

Permalink
feat(android-ndk): add api for getting debug images by addresses (#4089)
Browse files Browse the repository at this point in the history
* Add fetching debug images by addresses

* update test

* clean up

* update test

* update comments

* remove file

* fix imports

* update docs

* revert doc

* use hashset

* Format code

* update naming

* apiDump

* Improve Nullability, consider case of null imageSize

* Add fetching debug images by addresses

update test

clean up

update test

update comments

remove file

fix imports

update docs

revert doc

use hashset

Format code

update naming

apiDump

Improve Nullability, consider case of null imageSize

Update tests, ensure code_file and debug_file are set

* Fix test

* Update javadoc

* Update Changelog

---------

Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
Co-authored-by: Markus Hintersteiner <markus.hintersteiner@sentry.io>
  • Loading branch information
3 people authored Feb 11, 2025
1 parent f4162ef commit 5e31a6b
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Add split apks info to the `App` context ([#3193](https://github.com/getsentry/sentry-java/pull/3193))
- Expose new `withSentryObservableEffect` method overload that accepts `SentryNavigationListener` as a parameter ([#4143](https://github.com/getsentry/sentry-java/pull/4143))
- This allows sharing the same `SentryNavigationListener` instance across fragments and composables to preserve the trace
- (Internal) Add API to filter native debug images based on stacktrace addresses ([#4089](https://github.com/getsentry/sentry-java/pull/4089))

### Fixes

Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i
public abstract interface class io/sentry/android/core/IDebugImagesLoader {
public abstract fun clearDebugImages ()V
public abstract fun loadDebugImages ()Ljava/util/List;
public abstract fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
}

public final class io/sentry/android/core/InternalSentrySdk {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.sentry.protocol.DebugImage;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

Expand All @@ -11,5 +12,8 @@ public interface IDebugImagesLoader {
@Nullable
List<DebugImage> loadDebugImages();

@Nullable
Set<DebugImage> loadDebugImagesForAddresses(Set<String> addresses);

void clearDebugImages();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.sentry.protocol.DebugImage;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;

final class NoOpDebugImagesLoader implements IDebugImagesLoader {
Expand All @@ -19,6 +20,11 @@ public static NoOpDebugImagesLoader getInstance() {
return null;
}

@Override
public @Nullable Set<DebugImage> loadDebugImagesForAddresses(Set<String> addresses) {
return null;
}

@Override
public void clearDebugImages() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ class SentryAndroidOptionsTest {

private class CustomDebugImagesLoader : IDebugImagesLoader {
override fun loadDebugImages(): List<DebugImage>? = null
override fun loadDebugImagesForAddresses(addresses: Set<String>?): Set<DebugImage>? = null

override fun clearDebugImages() {}
}
}
1 change: 1 addition & 0 deletions sentry-android-ndk/api/sentry-android-ndk.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/c
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/ndk/NativeModuleListLoader;)V
public fun clearDebugImages ()V
public fun loadDebugImages ()Ljava/util/List;
public fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
}

public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.Objects;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
Expand All @@ -25,7 +27,7 @@ public final class DebugImagesLoader implements IDebugImagesLoader {

private final @NotNull NativeModuleListLoader moduleListLoader;

private static @Nullable List<DebugImage> debugImages;
private static volatile @Nullable List<DebugImage> debugImages;

/** we need to lock it because it could be called from different threads */
protected static final @NotNull AutoClosableReentrantLock debugImagesLock =
Expand Down Expand Up @@ -54,6 +56,8 @@ public DebugImagesLoader(
debugImages = new ArrayList<>(debugImagesArr.length);
for (io.sentry.ndk.DebugImage d : debugImagesArr) {
final DebugImage debugImage = new DebugImage();
debugImage.setCodeFile(d.getCodeFile());
debugImage.setDebugFile(d.getDebugFile());
debugImage.setUuid(d.getUuid());
debugImage.setType(d.getType());
debugImage.setDebugId(d.getDebugId());
Expand All @@ -75,7 +79,92 @@ public DebugImagesLoader(
return debugImages;
}

/** Clears the caching of debug images on sentry-native and here. */
/**
* Loads debug images for the given set of addresses.
*
* @param addresses Set of memory addresses to find debug images for
* @return Set of matching debug images, or null if debug images couldn't be loaded
*/
public @Nullable Set<DebugImage> loadDebugImagesForAddresses(
final @NotNull Set<String> addresses) {
try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) {
final @Nullable List<DebugImage> allDebugImages = loadDebugImages();
if (allDebugImages == null) {
return null;
}
if (addresses.isEmpty()) {
return null;
}

final Set<DebugImage> referencedImages = filterImagesByAddresses(allDebugImages, addresses);
if (referencedImages.isEmpty()) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"No debug images found for any of the %d addresses.",
addresses.size());
return null;
}

return referencedImages;
}
}

/**
* Finds all debug image containing the given addresses. Assumes that the images are sorted by
* address, which should always be true on Linux/Android and Windows platforms
*
* @return All matching debug images or null if none are found
*/
private @NotNull Set<DebugImage> filterImagesByAddresses(
final @NotNull List<DebugImage> images, final @NotNull Set<String> addresses) {
final Set<DebugImage> result = new HashSet<>();

for (int i = 0; i < images.size(); i++) {
final @NotNull DebugImage image = images.get(i);
final @Nullable DebugImage nextDebugImage =
(i + 1) < images.size() ? images.get(i + 1) : null;
final @Nullable String nextDebugImageAddress =
nextDebugImage != null ? nextDebugImage.getImageAddr() : null;

for (final @NotNull String rawAddress : addresses) {
try {
final long address = Long.parseLong(rawAddress.replace("0x", ""), 16);

final @Nullable String imageAddress = image.getImageAddr();
if (imageAddress != null) {
try {
final long imageStart = Long.parseLong(imageAddress.replace("0x", ""), 16);
final long imageEnd;

final @Nullable Long imageSize = image.getImageSize();
if (imageSize != null) {
imageEnd = imageStart + imageSize;
} else if (nextDebugImageAddress != null) {
imageEnd = Long.parseLong(nextDebugImageAddress.replace("0x", ""), 16);
} else {
imageEnd = Long.MAX_VALUE;
}
if (address >= imageStart && address < imageEnd) {
result.add(image);
// once image is added we can skip the remaining addresses and go straight to the
// next
// image
break;
}
} catch (NumberFormatException e) {
// ignored, invalid debug image address
}
}
} catch (NumberFormatException e) {
// ignored, invalid address supplied
}
}
}
return result;
}

@Override
public void clearDebugImages() {
try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
Expand All @@ -16,11 +17,13 @@ class DebugImagesLoaderTest {
val options = SentryAndroidOptions()

fun getSut(): DebugImagesLoader {
return DebugImagesLoader(options, nativeLoader)
val loader = DebugImagesLoader(options, nativeLoader)
loader.clearDebugImages()
return loader
}
}

private val fixture = Fixture()
private var fixture = Fixture()

@Test
fun `get images returns image list`() {
Expand Down Expand Up @@ -77,4 +80,107 @@ class DebugImagesLoaderTest {

assertNull(sut.cachedDebugImages)
}

@Test
fun `find images by address`() {
val sut = fixture.getSut()

val image1 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x1000"
imageSize = 0x1000L
}

val image2 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x2000"
imageSize = 0x1000L
}

val image3 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x3000"
imageSize = 0x1000L
}

whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3))

val result = sut.loadDebugImagesForAddresses(
setOf("0x1500", "0x2500")
)

assertNotNull(result)
assertEquals(2, result.size)
assertTrue(result.any { it.imageAddr == image1.imageAddr })
assertTrue(result.any { it.imageAddr == image2.imageAddr })
}

@Test
fun `find images with invalid addresses are not added to the result`() {
val sut = fixture.getSut()

val image1 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x1000"
imageSize = 0x1000L
}

val image2 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x2000"
imageSize = 0x1000L
}

whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2))

val hexAddresses = setOf("0xINVALID", "0x1500")
val result = sut.loadDebugImagesForAddresses(hexAddresses)

assertEquals(1, result!!.size)
}

@Test
fun `find images by address returns null if result is empty`() {
val sut = fixture.getSut()

val image1 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x1000"
imageSize = 0x1000L
}

val image2 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x2000"
imageSize = 0x1000L
}

whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2))

val hexAddresses = setOf("0x100", "0x10500")
val result = sut.loadDebugImagesForAddresses(hexAddresses)

assertNull(result)
}

@Test
fun `invalid image adresses are ignored for loadDebugImagesForAddresses`() {
val sut = fixture.getSut()

val image1 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0xNotANumber"
imageSize = 0x1000L
}

val image2 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x2000"
imageSize = null
}

val image3 = io.sentry.ndk.DebugImage().apply {
imageAddr = "0x5000"
imageSize = 0x1000L
}

whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3))

val hexAddresses = setOf("0x100", "0x2000", "0x2000", "0x5000")
val result = sut.loadDebugImagesForAddresses(hexAddresses)

assertNotNull(result)
assertEquals(2, result.size)
}
}

0 comments on commit 5e31a6b

Please sign in to comment.