Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
- Fix do not initialize SDK for Jetpack Compose Preview builds ([#4324](https://github.com/getsentry/sentry-java/pull/4324))
- Fix Synchronize Baggage values ([#4327](https://github.com/getsentry/sentry-java/pull/4327))

### Improvements

- Make `SystemEventsBreadcrumbsIntegration` faster ([#4330](https://github.com/getsentry/sentry-java/pull/4330))

## 8.7.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import io.sentry.util.StringUtils;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -55,19 +55,25 @@ public final class SystemEventsBreadcrumbsIntegration implements Integration, Cl

private @Nullable SentryAndroidOptions options;

private final @NotNull List<String> actions;
private final @NotNull String[] actions;
private boolean isClosed = false;
private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock();

public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) {
this(context, getDefaultActions());
this(context, getDefaultActionsInternal());
}

private SystemEventsBreadcrumbsIntegration(
final @NotNull Context context, final @NotNull String[] actions) {
this.context = ContextUtils.getApplicationContext(context);
this.actions = actions;
}

public SystemEventsBreadcrumbsIntegration(
final @NotNull Context context, final @NotNull List<String> actions) {
this.context =
Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required");
this.actions = Objects.requireNonNull(actions, "Actions list is required");
this.context = ContextUtils.getApplicationContext(context);
this.actions = new String[actions.size()];
actions.toArray(this.actions);
}

@Override
Expand Down Expand Up @@ -129,28 +135,32 @@ private void startSystemEventsReceiver(
}
}

@SuppressWarnings("deprecation")
public static @NotNull List<String> getDefaultActions() {
final List<String> actions = new ArrayList<>();
actions.add(ACTION_SHUTDOWN);
actions.add(ACTION_AIRPLANE_MODE_CHANGED);
actions.add(ACTION_BATTERY_CHANGED);
actions.add(ACTION_CAMERA_BUTTON);
actions.add(ACTION_CONFIGURATION_CHANGED);
actions.add(ACTION_DATE_CHANGED);
actions.add(ACTION_DEVICE_STORAGE_LOW);
actions.add(ACTION_DEVICE_STORAGE_OK);
actions.add(ACTION_DOCK_EVENT);
actions.add(ACTION_DREAMING_STARTED);
actions.add(ACTION_DREAMING_STOPPED);
actions.add(ACTION_INPUT_METHOD_CHANGED);
actions.add(ACTION_LOCALE_CHANGED);
actions.add(ACTION_SCREEN_OFF);
actions.add(ACTION_SCREEN_ON);
actions.add(ACTION_TIMEZONE_CHANGED);
actions.add(ACTION_TIME_CHANGED);
actions.add("android.os.action.DEVICE_IDLE_MODE_CHANGED");
actions.add("android.os.action.POWER_SAVE_MODE_CHANGED");
return Arrays.asList(getDefaultActionsInternal());
}

@SuppressWarnings("deprecation")
private static @NotNull String[] getDefaultActionsInternal() {
final String[] actions = new String[19];
actions[0] = ACTION_SHUTDOWN;
actions[1] = ACTION_AIRPLANE_MODE_CHANGED;
actions[2] = ACTION_BATTERY_CHANGED;
actions[3] = ACTION_CAMERA_BUTTON;
actions[4] = ACTION_CONFIGURATION_CHANGED;
actions[5] = ACTION_DATE_CHANGED;
actions[6] = ACTION_DEVICE_STORAGE_LOW;
actions[7] = ACTION_DEVICE_STORAGE_OK;
actions[8] = ACTION_DOCK_EVENT;
actions[9] = ACTION_DREAMING_STARTED;
actions[10] = ACTION_DREAMING_STOPPED;
actions[11] = ACTION_INPUT_METHOD_CHANGED;
actions[12] = ACTION_LOCALE_CHANGED;
actions[13] = ACTION_SCREEN_OFF;
actions[14] = ACTION_SCREEN_ON;
actions[15] = ACTION_TIMEZONE_CHANGED;
actions[16] = ACTION_TIME_CHANGED;
actions[17] = "android.os.action.DEVICE_IDLE_MODE_CHANGED";
actions[18] = "android.os.action.POWER_SAVE_MODE_CHANGED";
return actions;
}

Expand Down Expand Up @@ -206,10 +216,43 @@ public void onReceive(final Context context, final @NotNull Intent intent) {
scopes.addBreadcrumb(breadcrumb, hint);
});
} catch (Throwable t) {
options
.getLogger()
.log(SentryLevel.ERROR, t, "Failed to submit system event breadcrumb action.");
// ignored
}
}

// in theory this should be ThreadLocal, but we won't have more than 1 thread accessing it,
// so we save some memory here and CPU cycles. 64 is because all intent actions we subscribe for
// are less than 64 chars. We also don't care about encoding as those are always UTF.
// TODO: _MULTI_THREADED_EXECUTOR_
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decided to mark things that depend on the single-threaded executor from now on so we won't shoot ourselves in the foot if we ever gonna switch to a multi-threaded one 😅

private final char[] buf = new char[64];

@TestOnly
@Nullable
String getStringAfterDotFast(final @Nullable String str) {
if (str == null) {
return null;
}

final int len = str.length();
int bufIndex = buf.length;

// the idea here is to iterate from the end of the string and copy the characters to a
// pre-allocated buffer in reverse order. When we find a dot, we create a new string
// from the buffer. This way we use a fixed size buffer and do a bare minimum of iterations.
for (int i = len - 1; i >= 0; i--) {
final char c = str.charAt(i);
if (c == '.') {
return new String(buf, bufIndex, buf.length - bufIndex);
}
if (bufIndex == 0) {
// Overflow — fallback to safe version
return StringUtils.getStringAfterDot(str);
}
buf[--bufIndex] = c;
}

// No dot found — return original
return str;
}

private @NotNull Breadcrumb createBreadcrumb(
Expand All @@ -220,7 +263,7 @@ public void onReceive(final Context context, final @NotNull Intent intent) {
final Breadcrumb breadcrumb = new Breadcrumb(timeMs);
breadcrumb.setType("system");
breadcrumb.setCategory("device.event");
final String shortAction = StringUtils.getStringAfterDot(action);
final String shortAction = getStringAfterDotFast(action);
if (shortAction != null) {
breadcrumb.setData("action", shortAction);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class SystemEventsBreadcrumbsIntegrationTest {
sut.register(fixture.scopes, fixture.options)
val intent = Intent().apply {
action = Intent.ACTION_TIME_CHANGED
putExtra("test", 10)
putExtra("test2", 20)
}
sut.receiver!!.onReceive(fixture.context, intent)

Expand Down Expand Up @@ -183,4 +185,50 @@ class SystemEventsBreadcrumbsIntegrationTest {

assertFalse(fixture.options.isEnableSystemEventBreadcrumbs)
}

@Test
fun `when str has full package, return last string after dot`() {
val sut = fixture.getSut()

sut.register(fixture.scopes, fixture.options)

assertEquals("DEVICE_IDLE_MODE_CHANGED", sut.receiver?.getStringAfterDotFast("io.sentry.DEVICE_IDLE_MODE_CHANGED"))
assertEquals("POWER_SAVE_MODE_CHANGED", sut.receiver?.getStringAfterDotFast("io.sentry.POWER_SAVE_MODE_CHANGED"))
}

@Test
fun `when str is null, return null`() {
val sut = fixture.getSut()

sut.register(fixture.scopes, fixture.options)

assertNull(sut.receiver?.getStringAfterDotFast(null))
}

@Test
fun `when str is empty, return the original str`() {
val sut = fixture.getSut()

sut.register(fixture.scopes, fixture.options)

assertEquals("", sut.receiver?.getStringAfterDotFast(""))
}

@Test
fun `when str ends with a dot, return empty str`() {
val sut = fixture.getSut()

sut.register(fixture.scopes, fixture.options)

assertEquals("", sut.receiver?.getStringAfterDotFast("io.sentry."))
}

@Test
fun `when str has no dots, return the original str`() {
val sut = fixture.getSut()

sut.register(fixture.scopes, fixture.options)

assertEquals("iosentry", sut.receiver?.getStringAfterDotFast("iosentry"))
}
}
Loading