Skip to content

Commit 175c4af

Browse files
authored
perf(breadcrumbs): Make SystemEventsBreadcrumbsIntegration faster (#4330)
* Make SystemEventsBreadcrumbsIntegration faster * Changelog * Fix leak
1 parent 34b5305 commit 175c4af

File tree

3 files changed

+126
-31
lines changed

3 files changed

+126
-31
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
- Fix do not initialize SDK for Jetpack Compose Preview builds ([#4324](https://github.com/getsentry/sentry-java/pull/4324))
1818
- Fix Synchronize Baggage values ([#4327](https://github.com/getsentry/sentry-java/pull/4327))
1919

20+
### Improvements
21+
22+
- Make `SystemEventsBreadcrumbsIntegration` faster ([#4330](https://github.com/getsentry/sentry-java/pull/4330))
23+
2024
## 8.7.0
2125

2226
### Features

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

+74-31
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
import io.sentry.util.StringUtils;
4040
import java.io.Closeable;
4141
import java.io.IOException;
42-
import java.util.ArrayList;
42+
import java.util.Arrays;
4343
import java.util.HashMap;
4444
import java.util.List;
4545
import java.util.Map;
@@ -55,19 +55,25 @@ public final class SystemEventsBreadcrumbsIntegration implements Integration, Cl
5555

5656
private @Nullable SentryAndroidOptions options;
5757

58-
private final @NotNull List<String> actions;
58+
private final @NotNull String[] actions;
5959
private boolean isClosed = false;
6060
private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock();
6161

6262
public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) {
63-
this(context, getDefaultActions());
63+
this(context, getDefaultActionsInternal());
64+
}
65+
66+
private SystemEventsBreadcrumbsIntegration(
67+
final @NotNull Context context, final @NotNull String[] actions) {
68+
this.context = ContextUtils.getApplicationContext(context);
69+
this.actions = actions;
6470
}
6571

6672
public SystemEventsBreadcrumbsIntegration(
6773
final @NotNull Context context, final @NotNull List<String> actions) {
68-
this.context =
69-
Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required");
70-
this.actions = Objects.requireNonNull(actions, "Actions list is required");
74+
this.context = ContextUtils.getApplicationContext(context);
75+
this.actions = new String[actions.size()];
76+
actions.toArray(this.actions);
7177
}
7278

7379
@Override
@@ -129,28 +135,32 @@ private void startSystemEventsReceiver(
129135
}
130136
}
131137

132-
@SuppressWarnings("deprecation")
133138
public static @NotNull List<String> getDefaultActions() {
134-
final List<String> actions = new ArrayList<>();
135-
actions.add(ACTION_SHUTDOWN);
136-
actions.add(ACTION_AIRPLANE_MODE_CHANGED);
137-
actions.add(ACTION_BATTERY_CHANGED);
138-
actions.add(ACTION_CAMERA_BUTTON);
139-
actions.add(ACTION_CONFIGURATION_CHANGED);
140-
actions.add(ACTION_DATE_CHANGED);
141-
actions.add(ACTION_DEVICE_STORAGE_LOW);
142-
actions.add(ACTION_DEVICE_STORAGE_OK);
143-
actions.add(ACTION_DOCK_EVENT);
144-
actions.add(ACTION_DREAMING_STARTED);
145-
actions.add(ACTION_DREAMING_STOPPED);
146-
actions.add(ACTION_INPUT_METHOD_CHANGED);
147-
actions.add(ACTION_LOCALE_CHANGED);
148-
actions.add(ACTION_SCREEN_OFF);
149-
actions.add(ACTION_SCREEN_ON);
150-
actions.add(ACTION_TIMEZONE_CHANGED);
151-
actions.add(ACTION_TIME_CHANGED);
152-
actions.add("android.os.action.DEVICE_IDLE_MODE_CHANGED");
153-
actions.add("android.os.action.POWER_SAVE_MODE_CHANGED");
139+
return Arrays.asList(getDefaultActionsInternal());
140+
}
141+
142+
@SuppressWarnings("deprecation")
143+
private static @NotNull String[] getDefaultActionsInternal() {
144+
final String[] actions = new String[19];
145+
actions[0] = ACTION_SHUTDOWN;
146+
actions[1] = ACTION_AIRPLANE_MODE_CHANGED;
147+
actions[2] = ACTION_BATTERY_CHANGED;
148+
actions[3] = ACTION_CAMERA_BUTTON;
149+
actions[4] = ACTION_CONFIGURATION_CHANGED;
150+
actions[5] = ACTION_DATE_CHANGED;
151+
actions[6] = ACTION_DEVICE_STORAGE_LOW;
152+
actions[7] = ACTION_DEVICE_STORAGE_OK;
153+
actions[8] = ACTION_DOCK_EVENT;
154+
actions[9] = ACTION_DREAMING_STARTED;
155+
actions[10] = ACTION_DREAMING_STOPPED;
156+
actions[11] = ACTION_INPUT_METHOD_CHANGED;
157+
actions[12] = ACTION_LOCALE_CHANGED;
158+
actions[13] = ACTION_SCREEN_OFF;
159+
actions[14] = ACTION_SCREEN_ON;
160+
actions[15] = ACTION_TIMEZONE_CHANGED;
161+
actions[16] = ACTION_TIME_CHANGED;
162+
actions[17] = "android.os.action.DEVICE_IDLE_MODE_CHANGED";
163+
actions[18] = "android.os.action.POWER_SAVE_MODE_CHANGED";
154164
return actions;
155165
}
156166

@@ -206,10 +216,43 @@ public void onReceive(final Context context, final @NotNull Intent intent) {
206216
scopes.addBreadcrumb(breadcrumb, hint);
207217
});
208218
} catch (Throwable t) {
209-
options
210-
.getLogger()
211-
.log(SentryLevel.ERROR, t, "Failed to submit system event breadcrumb action.");
219+
// ignored
220+
}
221+
}
222+
223+
// in theory this should be ThreadLocal, but we won't have more than 1 thread accessing it,
224+
// so we save some memory here and CPU cycles. 64 is because all intent actions we subscribe for
225+
// are less than 64 chars. We also don't care about encoding as those are always UTF.
226+
// TODO: _MULTI_THREADED_EXECUTOR_
227+
private final char[] buf = new char[64];
228+
229+
@TestOnly
230+
@Nullable
231+
String getStringAfterDotFast(final @Nullable String str) {
232+
if (str == null) {
233+
return null;
212234
}
235+
236+
final int len = str.length();
237+
int bufIndex = buf.length;
238+
239+
// the idea here is to iterate from the end of the string and copy the characters to a
240+
// pre-allocated buffer in reverse order. When we find a dot, we create a new string
241+
// from the buffer. This way we use a fixed size buffer and do a bare minimum of iterations.
242+
for (int i = len - 1; i >= 0; i--) {
243+
final char c = str.charAt(i);
244+
if (c == '.') {
245+
return new String(buf, bufIndex, buf.length - bufIndex);
246+
}
247+
if (bufIndex == 0) {
248+
// Overflow — fallback to safe version
249+
return StringUtils.getStringAfterDot(str);
250+
}
251+
buf[--bufIndex] = c;
252+
}
253+
254+
// No dot found — return original
255+
return str;
213256
}
214257

215258
private @NotNull Breadcrumb createBreadcrumb(
@@ -220,7 +263,7 @@ public void onReceive(final Context context, final @NotNull Intent intent) {
220263
final Breadcrumb breadcrumb = new Breadcrumb(timeMs);
221264
breadcrumb.setType("system");
222265
breadcrumb.setCategory("device.event");
223-
final String shortAction = StringUtils.getStringAfterDot(action);
266+
final String shortAction = getStringAfterDotFast(action);
224267
if (shortAction != null) {
225268
breadcrumb.setData("action", shortAction);
226269
}

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

+48
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ class SystemEventsBreadcrumbsIntegrationTest {
105105
sut.register(fixture.scopes, fixture.options)
106106
val intent = Intent().apply {
107107
action = Intent.ACTION_TIME_CHANGED
108+
putExtra("test", 10)
109+
putExtra("test2", 20)
108110
}
109111
sut.receiver!!.onReceive(fixture.context, intent)
110112

@@ -183,4 +185,50 @@ class SystemEventsBreadcrumbsIntegrationTest {
183185

184186
assertFalse(fixture.options.isEnableSystemEventBreadcrumbs)
185187
}
188+
189+
@Test
190+
fun `when str has full package, return last string after dot`() {
191+
val sut = fixture.getSut()
192+
193+
sut.register(fixture.scopes, fixture.options)
194+
195+
assertEquals("DEVICE_IDLE_MODE_CHANGED", sut.receiver?.getStringAfterDotFast("io.sentry.DEVICE_IDLE_MODE_CHANGED"))
196+
assertEquals("POWER_SAVE_MODE_CHANGED", sut.receiver?.getStringAfterDotFast("io.sentry.POWER_SAVE_MODE_CHANGED"))
197+
}
198+
199+
@Test
200+
fun `when str is null, return null`() {
201+
val sut = fixture.getSut()
202+
203+
sut.register(fixture.scopes, fixture.options)
204+
205+
assertNull(sut.receiver?.getStringAfterDotFast(null))
206+
}
207+
208+
@Test
209+
fun `when str is empty, return the original str`() {
210+
val sut = fixture.getSut()
211+
212+
sut.register(fixture.scopes, fixture.options)
213+
214+
assertEquals("", sut.receiver?.getStringAfterDotFast(""))
215+
}
216+
217+
@Test
218+
fun `when str ends with a dot, return empty str`() {
219+
val sut = fixture.getSut()
220+
221+
sut.register(fixture.scopes, fixture.options)
222+
223+
assertEquals("", sut.receiver?.getStringAfterDotFast("io.sentry."))
224+
}
225+
226+
@Test
227+
fun `when str has no dots, return the original str`() {
228+
val sut = fixture.getSut()
229+
230+
sut.register(fixture.scopes, fixture.options)
231+
232+
assertEquals("iosentry", sut.receiver?.getStringAfterDotFast("iosentry"))
233+
}
186234
}

0 commit comments

Comments
 (0)