Skip to content

Commit 5e31a6b

Browse files
buenaflorgetsentry-botmarkushi
authored
feat(android-ndk): add api for getting debug images by addresses (#4089)
* 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 <[email protected]> Co-authored-by: Markus Hintersteiner <[email protected]>
1 parent f4162ef commit 5e31a6b

File tree

8 files changed

+214
-4
lines changed

8 files changed

+214
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- Add split apks info to the `App` context ([#3193](https://github.com/getsentry/sentry-java/pull/3193))
1313
- Expose new `withSentryObservableEffect` method overload that accepts `SentryNavigationListener` as a parameter ([#4143](https://github.com/getsentry/sentry-java/pull/4143))
1414
- This allows sharing the same `SentryNavigationListener` instance across fragments and composables to preserve the trace
15+
- (Internal) Add API to filter native debug images based on stacktrace addresses ([#4089](https://github.com/getsentry/sentry-java/pull/4089))
1516

1617
### Fixes
1718

sentry-android-core/api/sentry-android-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i
212212
public abstract interface class io/sentry/android/core/IDebugImagesLoader {
213213
public abstract fun clearDebugImages ()V
214214
public abstract fun loadDebugImages ()Ljava/util/List;
215+
public abstract fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
215216
}
216217

217218
public final class io/sentry/android/core/InternalSentrySdk {

sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.sentry.protocol.DebugImage;
44
import java.util.List;
5+
import java.util.Set;
56
import org.jetbrains.annotations.ApiStatus;
67
import org.jetbrains.annotations.Nullable;
78

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

15+
@Nullable
16+
Set<DebugImage> loadDebugImagesForAddresses(Set<String> addresses);
17+
1418
void clearDebugImages();
1519
}

sentry-android-core/src/main/java/io/sentry/android/core/NoOpDebugImagesLoader.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.sentry.protocol.DebugImage;
44
import java.util.List;
5+
import java.util.Set;
56
import org.jetbrains.annotations.Nullable;
67

78
final class NoOpDebugImagesLoader implements IDebugImagesLoader {
@@ -19,6 +20,11 @@ public static NoOpDebugImagesLoader getInstance() {
1920
return null;
2021
}
2122

23+
@Override
24+
public @Nullable Set<DebugImage> loadDebugImagesForAddresses(Set<String> addresses) {
25+
return null;
26+
}
27+
2228
@Override
2329
public void clearDebugImages() {}
2430
}

sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ class SentryAndroidOptionsTest {
182182

183183
private class CustomDebugImagesLoader : IDebugImagesLoader {
184184
override fun loadDebugImages(): List<DebugImage>? = null
185+
override fun loadDebugImagesForAddresses(addresses: Set<String>?): Set<DebugImage>? = null
186+
185187
override fun clearDebugImages() {}
186188
}
187189
}

sentry-android-ndk/api/sentry-android-ndk.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/c
1010
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/ndk/NativeModuleListLoader;)V
1111
public fun clearDebugImages ()V
1212
public fun loadDebugImages ()Ljava/util/List;
13+
public fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
1314
}
1415

1516
public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter {

sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import io.sentry.util.AutoClosableReentrantLock;
1111
import io.sentry.util.Objects;
1212
import java.util.ArrayList;
13+
import java.util.HashSet;
1314
import java.util.List;
15+
import java.util.Set;
1416
import org.jetbrains.annotations.NotNull;
1517
import org.jetbrains.annotations.Nullable;
1618
import org.jetbrains.annotations.VisibleForTesting;
@@ -25,7 +27,7 @@ public final class DebugImagesLoader implements IDebugImagesLoader {
2527

2628
private final @NotNull NativeModuleListLoader moduleListLoader;
2729

28-
private static @Nullable List<DebugImage> debugImages;
30+
private static volatile @Nullable List<DebugImage> debugImages;
2931

3032
/** we need to lock it because it could be called from different threads */
3133
protected static final @NotNull AutoClosableReentrantLock debugImagesLock =
@@ -54,6 +56,8 @@ public DebugImagesLoader(
5456
debugImages = new ArrayList<>(debugImagesArr.length);
5557
for (io.sentry.ndk.DebugImage d : debugImagesArr) {
5658
final DebugImage debugImage = new DebugImage();
59+
debugImage.setCodeFile(d.getCodeFile());
60+
debugImage.setDebugFile(d.getDebugFile());
5761
debugImage.setUuid(d.getUuid());
5862
debugImage.setType(d.getType());
5963
debugImage.setDebugId(d.getDebugId());
@@ -75,7 +79,92 @@ public DebugImagesLoader(
7579
return debugImages;
7680
}
7781

78-
/** Clears the caching of debug images on sentry-native and here. */
82+
/**
83+
* Loads debug images for the given set of addresses.
84+
*
85+
* @param addresses Set of memory addresses to find debug images for
86+
* @return Set of matching debug images, or null if debug images couldn't be loaded
87+
*/
88+
public @Nullable Set<DebugImage> loadDebugImagesForAddresses(
89+
final @NotNull Set<String> addresses) {
90+
try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) {
91+
final @Nullable List<DebugImage> allDebugImages = loadDebugImages();
92+
if (allDebugImages == null) {
93+
return null;
94+
}
95+
if (addresses.isEmpty()) {
96+
return null;
97+
}
98+
99+
final Set<DebugImage> referencedImages = filterImagesByAddresses(allDebugImages, addresses);
100+
if (referencedImages.isEmpty()) {
101+
options
102+
.getLogger()
103+
.log(
104+
SentryLevel.WARNING,
105+
"No debug images found for any of the %d addresses.",
106+
addresses.size());
107+
return null;
108+
}
109+
110+
return referencedImages;
111+
}
112+
}
113+
114+
/**
115+
* Finds all debug image containing the given addresses. Assumes that the images are sorted by
116+
* address, which should always be true on Linux/Android and Windows platforms
117+
*
118+
* @return All matching debug images or null if none are found
119+
*/
120+
private @NotNull Set<DebugImage> filterImagesByAddresses(
121+
final @NotNull List<DebugImage> images, final @NotNull Set<String> addresses) {
122+
final Set<DebugImage> result = new HashSet<>();
123+
124+
for (int i = 0; i < images.size(); i++) {
125+
final @NotNull DebugImage image = images.get(i);
126+
final @Nullable DebugImage nextDebugImage =
127+
(i + 1) < images.size() ? images.get(i + 1) : null;
128+
final @Nullable String nextDebugImageAddress =
129+
nextDebugImage != null ? nextDebugImage.getImageAddr() : null;
130+
131+
for (final @NotNull String rawAddress : addresses) {
132+
try {
133+
final long address = Long.parseLong(rawAddress.replace("0x", ""), 16);
134+
135+
final @Nullable String imageAddress = image.getImageAddr();
136+
if (imageAddress != null) {
137+
try {
138+
final long imageStart = Long.parseLong(imageAddress.replace("0x", ""), 16);
139+
final long imageEnd;
140+
141+
final @Nullable Long imageSize = image.getImageSize();
142+
if (imageSize != null) {
143+
imageEnd = imageStart + imageSize;
144+
} else if (nextDebugImageAddress != null) {
145+
imageEnd = Long.parseLong(nextDebugImageAddress.replace("0x", ""), 16);
146+
} else {
147+
imageEnd = Long.MAX_VALUE;
148+
}
149+
if (address >= imageStart && address < imageEnd) {
150+
result.add(image);
151+
// once image is added we can skip the remaining addresses and go straight to the
152+
// next
153+
// image
154+
break;
155+
}
156+
} catch (NumberFormatException e) {
157+
// ignored, invalid debug image address
158+
}
159+
}
160+
} catch (NumberFormatException e) {
161+
// ignored, invalid address supplied
162+
}
163+
}
164+
}
165+
return result;
166+
}
167+
79168
@Override
80169
public void clearDebugImages() {
81170
try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) {

sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.mockito.kotlin.mock
66
import org.mockito.kotlin.verify
77
import org.mockito.kotlin.whenever
88
import kotlin.test.Test
9+
import kotlin.test.assertEquals
910
import kotlin.test.assertNotNull
1011
import kotlin.test.assertNull
1112
import kotlin.test.assertTrue
@@ -16,11 +17,13 @@ class DebugImagesLoaderTest {
1617
val options = SentryAndroidOptions()
1718

1819
fun getSut(): DebugImagesLoader {
19-
return DebugImagesLoader(options, nativeLoader)
20+
val loader = DebugImagesLoader(options, nativeLoader)
21+
loader.clearDebugImages()
22+
return loader
2023
}
2124
}
2225

23-
private val fixture = Fixture()
26+
private var fixture = Fixture()
2427

2528
@Test
2629
fun `get images returns image list`() {
@@ -77,4 +80,107 @@ class DebugImagesLoaderTest {
7780

7881
assertNull(sut.cachedDebugImages)
7982
}
83+
84+
@Test
85+
fun `find images by address`() {
86+
val sut = fixture.getSut()
87+
88+
val image1 = io.sentry.ndk.DebugImage().apply {
89+
imageAddr = "0x1000"
90+
imageSize = 0x1000L
91+
}
92+
93+
val image2 = io.sentry.ndk.DebugImage().apply {
94+
imageAddr = "0x2000"
95+
imageSize = 0x1000L
96+
}
97+
98+
val image3 = io.sentry.ndk.DebugImage().apply {
99+
imageAddr = "0x3000"
100+
imageSize = 0x1000L
101+
}
102+
103+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3))
104+
105+
val result = sut.loadDebugImagesForAddresses(
106+
setOf("0x1500", "0x2500")
107+
)
108+
109+
assertNotNull(result)
110+
assertEquals(2, result.size)
111+
assertTrue(result.any { it.imageAddr == image1.imageAddr })
112+
assertTrue(result.any { it.imageAddr == image2.imageAddr })
113+
}
114+
115+
@Test
116+
fun `find images with invalid addresses are not added to the result`() {
117+
val sut = fixture.getSut()
118+
119+
val image1 = io.sentry.ndk.DebugImage().apply {
120+
imageAddr = "0x1000"
121+
imageSize = 0x1000L
122+
}
123+
124+
val image2 = io.sentry.ndk.DebugImage().apply {
125+
imageAddr = "0x2000"
126+
imageSize = 0x1000L
127+
}
128+
129+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2))
130+
131+
val hexAddresses = setOf("0xINVALID", "0x1500")
132+
val result = sut.loadDebugImagesForAddresses(hexAddresses)
133+
134+
assertEquals(1, result!!.size)
135+
}
136+
137+
@Test
138+
fun `find images by address returns null if result is empty`() {
139+
val sut = fixture.getSut()
140+
141+
val image1 = io.sentry.ndk.DebugImage().apply {
142+
imageAddr = "0x1000"
143+
imageSize = 0x1000L
144+
}
145+
146+
val image2 = io.sentry.ndk.DebugImage().apply {
147+
imageAddr = "0x2000"
148+
imageSize = 0x1000L
149+
}
150+
151+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2))
152+
153+
val hexAddresses = setOf("0x100", "0x10500")
154+
val result = sut.loadDebugImagesForAddresses(hexAddresses)
155+
156+
assertNull(result)
157+
}
158+
159+
@Test
160+
fun `invalid image adresses are ignored for loadDebugImagesForAddresses`() {
161+
val sut = fixture.getSut()
162+
163+
val image1 = io.sentry.ndk.DebugImage().apply {
164+
imageAddr = "0xNotANumber"
165+
imageSize = 0x1000L
166+
}
167+
168+
val image2 = io.sentry.ndk.DebugImage().apply {
169+
imageAddr = "0x2000"
170+
imageSize = null
171+
}
172+
173+
val image3 = io.sentry.ndk.DebugImage().apply {
174+
imageAddr = "0x5000"
175+
imageSize = 0x1000L
176+
}
177+
178+
whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3))
179+
180+
val hexAddresses = setOf("0x100", "0x2000", "0x2000", "0x5000")
181+
val result = sut.loadDebugImagesForAddresses(hexAddresses)
182+
183+
assertNotNull(result)
184+
assertEquals(2, result.size)
185+
}
80186
}

0 commit comments

Comments
 (0)