Skip to content

Commit d714dc8

Browse files
markushibuenaflorgetsentry-bot
authored
feat(android-ndk): add api for getting debug images by addresses (#4159)
* feat(android-ndk): add api for getting debug images by addresses (#4089) --------- Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io> Co-authored-by: Markus Hintersteiner <markus.hintersteiner@sentry.io> * Update Changelog * Format code * Fix switch sync/data classes to match 7.x.x --------- Co-authored-by: Giancarlo Buenaflor <giancarlo_buenaflor@yahoo.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent 1eac2fc commit d714dc8

File tree

8 files changed

+215
-5
lines changed

8 files changed

+215
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
- (Jetpack Compose) Modifier.sentryTag now uses Modifier.Node ([#4029](https://github.com/getsentry/sentry-java/pull/4029))
1212
- This allows Composables that use this modifier to be skippable
1313

14+
### Features
15+
16+
- (Internal) Add API to filter native debug images based on stacktrace addresses ([#4159](https://github.com/getsentry/sentry-java/pull/4159))
17+
1418
## 7.21.0
1519

1620
### Fixes

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i
208208
public abstract interface class io/sentry/android/core/IDebugImagesLoader {
209209
public abstract fun clearDebugImages ()V
210210
public abstract fun loadDebugImages ()Ljava/util/List;
211+
public abstract fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
211212
}
212213

213214
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/android/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: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import io.sentry.protocol.DebugImage;
88
import io.sentry.util.Objects;
99
import java.util.Arrays;
10+
import java.util.HashSet;
1011
import java.util.List;
12+
import java.util.Set;
1113
import org.jetbrains.annotations.NotNull;
1214
import org.jetbrains.annotations.Nullable;
1315
import org.jetbrains.annotations.VisibleForTesting;
@@ -22,7 +24,7 @@ public final class DebugImagesLoader implements IDebugImagesLoader {
2224

2325
private final @NotNull NativeModuleListLoader moduleListLoader;
2426

25-
private static @Nullable List<DebugImage> debugImages;
27+
private static volatile @Nullable List<DebugImage> debugImages;
2628

2729
/** we need to lock it because it could be called from different threads */
2830
private static final @NotNull Object debugImagesLock = new Object();
@@ -60,7 +62,92 @@ public DebugImagesLoader(
6062
return debugImages;
6163
}
6264

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

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

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import io.sentry.protocol.DebugImage
55
import org.mockito.kotlin.mock
66
import org.mockito.kotlin.verify
77
import org.mockito.kotlin.whenever
8-
import java.lang.RuntimeException
98
import kotlin.test.Test
9+
import kotlin.test.assertEquals
1010
import kotlin.test.assertNotNull
1111
import kotlin.test.assertNull
1212
import kotlin.test.assertTrue
@@ -17,11 +17,13 @@ class DebugImagesLoaderTest {
1717
val options = SentryAndroidOptions()
1818

1919
fun getSut(): DebugImagesLoader {
20-
return DebugImagesLoader(options, nativeLoader)
20+
val loader = DebugImagesLoader(options, nativeLoader)
21+
loader.clearDebugImages()
22+
return loader
2123
}
2224
}
2325

24-
private val fixture = Fixture()
26+
private var fixture = Fixture()
2527

2628
@Test
2729
fun `get images returns image list`() {
@@ -78,4 +80,107 @@ class DebugImagesLoaderTest {
7880

7981
assertNull(sut.cachedDebugImages)
8082
}
83+
84+
@Test
85+
fun `find images by address`() {
86+
val sut = fixture.getSut()
87+
88+
val image1 = DebugImage().apply {
89+
imageAddr = "0x1000"
90+
imageSize = 0x1000L
91+
}
92+
93+
val image2 = DebugImage().apply {
94+
imageAddr = "0x2000"
95+
imageSize = 0x1000L
96+
}
97+
98+
val image3 = 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 = DebugImage().apply {
120+
imageAddr = "0x1000"
121+
imageSize = 0x1000L
122+
}
123+
124+
val image2 = 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 = DebugImage().apply {
142+
imageAddr = "0x1000"
143+
imageSize = 0x1000L
144+
}
145+
146+
val image2 = 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 addresses are ignored for loadDebugImagesForAddresses`() {
161+
val sut = fixture.getSut()
162+
163+
val image1 = DebugImage().apply {
164+
imageAddr = "0xNotANumber"
165+
imageSize = 0x1000L
166+
}
167+
168+
val image2 = DebugImage().apply {
169+
imageAddr = "0x2000"
170+
imageSize = null
171+
}
172+
173+
val image3 = 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+
}
81186
}

0 commit comments

Comments
 (0)