Skip to content

Commit 2ed8422

Browse files
committed
perf(breadcrumbs): Make SystemEventsBreadcrumbsIntegration faster (#4330)
* Make SystemEventsBreadcrumbsIntegration faster * Changelog * Fix leak
1 parent 91015a0 commit 2ed8422

3 files changed

Lines changed: 123 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Compress Screenshots on a background thread ([#4295](https://github.com/getsentry/sentry-java/pull/4295))
88
- Improve low memory breadcrumb capturing ([#4325](https://github.com/getsentry/sentry-java/pull/4325))
9+
- Make `SystemEventsBreadcrumbsIntegration` faster ([#4330](https://github.com/getsentry/sentry-java/pull/4330))
910

1011
## 7.22.5
1112

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

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import io.sentry.util.StringUtils;
3838
import java.io.Closeable;
3939
import java.io.IOException;
40-
import java.util.ArrayList;
40+
import java.util.Arrays;
4141
import java.util.HashMap;
4242
import java.util.List;
4343
import java.util.Map;
@@ -53,19 +53,25 @@ public final class SystemEventsBreadcrumbsIntegration implements Integration, Cl
5353

5454
private @Nullable SentryAndroidOptions options;
5555

56-
private final @NotNull List<String> actions;
56+
private final @NotNull String[] actions;
5757
private boolean isClosed = false;
5858
private final @NotNull Object startLock = new Object();
5959

6060
public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) {
61-
this(context, getDefaultActions());
61+
this(context, getDefaultActionsInternal());
62+
}
63+
64+
private SystemEventsBreadcrumbsIntegration(
65+
final @NotNull Context context, final @NotNull String[] actions) {
66+
this.context = ContextUtils.getApplicationContext(context);
67+
this.actions = actions;
6268
}
6369

6470
public SystemEventsBreadcrumbsIntegration(
6571
final @NotNull Context context, final @NotNull List<String> actions) {
66-
this.context =
67-
Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required");
68-
this.actions = Objects.requireNonNull(actions, "Actions list is required");
72+
this.context = ContextUtils.getApplicationContext(context);
73+
this.actions = new String[actions.size()];
74+
actions.toArray(this.actions);
6975
}
7076

7177
@Override
@@ -127,28 +133,32 @@ private void startSystemEventsReceiver(
127133
}
128134
}
129135

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

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

213256
private @NotNull Breadcrumb createBreadcrumb(
@@ -218,7 +261,7 @@ public void onReceive(final Context context, final @NotNull Intent intent) {
218261
final Breadcrumb breadcrumb = new Breadcrumb(timeMs);
219262
breadcrumb.setType("system");
220263
breadcrumb.setCategory("device.event");
221-
final String shortAction = StringUtils.getStringAfterDot(action);
264+
final String shortAction = getStringAfterDotFast(action);
222265
if (shortAction != null) {
223266
breadcrumb.setData("action", shortAction);
224267
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ class SystemEventsBreadcrumbsIntegrationTest {
102102
sut.register(fixture.hub, fixture.options)
103103
val intent = Intent().apply {
104104
action = Intent.ACTION_TIME_CHANGED
105+
putExtra("test", 10)
106+
putExtra("test2", 20)
105107
}
106108
sut.receiver!!.onReceive(fixture.context, intent)
107109

@@ -180,4 +182,50 @@ class SystemEventsBreadcrumbsIntegrationTest {
180182

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

0 commit comments

Comments
 (0)