From a33ba569cdfb4e20e21b548b7830f035a3fcff74 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Jun 2026 17:23:06 +0200 Subject: [PATCH 1/4] perf(core): Use fixed-delay scheduling for performance collector Switch the transaction collection timer from scheduleAtFixedRate to schedule. Fixed-rate scheduling fires rapid catch-up executions after a delay or GC pause, which the old code guarded against with a 10ms skip check. Fixed-delay scheduling spaces each collection 100ms after the previous one finishes, so the catch-up bursts cannot happen and the guard, its timestamp field, and the stale comment are no longer needed. Co-Authored-By: Claude Opus 4.8 --- .../sentry/DefaultCompositePerformanceCollector.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java index 1861381a853..4736ab8fac5 100644 --- a/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java @@ -27,7 +27,6 @@ public final class DefaultCompositePerformanceCollector implements CompositePerf private final @NotNull SentryOptions options; private final @NotNull AtomicBoolean isStarted = new AtomicBoolean(false); - private long lastCollectionTimestamp = 0; public DefaultCompositePerformanceCollector(final @NotNull SentryOptions options) { this.options = Objects.requireNonNull(options, "The options object is required."); @@ -112,16 +111,8 @@ public void run() { new TimerTask() { @Override public void run() { - long now = System.currentTimeMillis(); - // The timer is scheduled to run every 100ms on average. In case it takes longer, - // subsequent tasks are executed more quickly. If two tasks are scheduled to run in - // less than 10ms, the measurement that we collect is not meaningful, so we skip it - if (now - lastCollectionTimestamp <= 10) { - return; - } timedOutTransactions.clear(); - lastCollectionTimestamp = now; final @NotNull PerformanceCollectionData tempData = new PerformanceCollectionData(options.getDateProvider().now().nanoTimestamp()); @@ -147,7 +138,7 @@ public void run() { } } }; - timer.scheduleAtFixedRate( + timer.schedule( timerTask, TRANSACTION_COLLECTION_INTERVAL_MILLIS, TRANSACTION_COLLECTION_INTERVAL_MILLIS); From c12e284614d0e30934c230271573aadcdef67965 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Jun 2026 17:24:22 +0200 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a813a8e7b6..22d2849ce88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Improve SDK init performance by replacing `java.net.URI` with custom string parsing for DSN ([#5448](https://github.com/getsentry/sentry-java/pull/5448)) - Remove unnecessary boxing to improve performance ([#5520](https://github.com/getsentry/sentry-java/pull/5520)) +- Use fixed-delay scheduling for the performance collector to avoid catch-up collection bursts ([#5524](https://github.com/getsentry/sentry-java/pull/5524)) ### Fixes From c85393205e23b4484815fdb38dcd49ecd7393d90 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Jun 2026 17:39:50 +0200 Subject: [PATCH 3/4] test(core): Verify schedule instead of scheduleAtFixedRate The performance collector now uses fixed-delay scheduling, so the timer verifications assert schedule(...) rather than scheduleAtFixedRate(...). Co-Authored-By: Claude Opus 4.8 --- .../DefaultCompositePerformanceCollectorTest.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt index 46c304358df..ceec3571ebd 100644 --- a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt @@ -86,7 +86,7 @@ class DefaultCompositePerformanceCollectorTest { val collector = fixture.getSut(null, null) assertTrue(fixture.options.performanceCollectors.isEmpty()) collector.start(fixture.transaction1) - verify(fixture.mockTimer, never())!!.scheduleAtFixedRate(any(), any(), any()) + verify(fixture.mockTimer, never())!!.schedule(any(), any(), any()) } @Test @@ -104,14 +104,14 @@ class DefaultCompositePerformanceCollectorTest { fun `when start, timer is scheduled every 100 milliseconds`() { val collector = fixture.getSut() collector.start(fixture.transaction1) - verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer)!!.schedule(any(), any(), eq(100)) } @Test fun `when start with a string, timer is scheduled every 100 milliseconds`() { val collector = fixture.getSut() collector.start(fixture.id1) - verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer)!!.schedule(any(), any(), eq(100)) } @Test @@ -119,7 +119,7 @@ class DefaultCompositePerformanceCollectorTest { val collector = fixture.getSut() collector.start(fixture.transaction1) collector.stop(fixture.transaction1) - verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer)!!.schedule(any(), any(), eq(100)) verify(fixture.mockTimer)!!.cancel() } @@ -128,7 +128,7 @@ class DefaultCompositePerformanceCollectorTest { val collector = fixture.getSut() collector.start(fixture.id1) collector.stop(fixture.id1) - verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer)!!.schedule(any(), any(), eq(100)) verify(fixture.mockTimer)!!.cancel() } @@ -136,7 +136,7 @@ class DefaultCompositePerformanceCollectorTest { fun `stopping a not collected transaction return null`() { val collector = fixture.getSut() val data = collector.stop(fixture.transaction1) - verify(fixture.mockTimer, never())!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer, never())!!.schedule(any(), any(), eq(100)) verify(fixture.mockTimer, never())!!.cancel() assertNull(data) } @@ -145,7 +145,7 @@ class DefaultCompositePerformanceCollectorTest { fun `stopping a not collected id return null`() { val collector = fixture.getSut() val data = collector.stop(fixture.id1) - verify(fixture.mockTimer, never())!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer, never())!!.schedule(any(), any(), eq(100)) verify(fixture.mockTimer, never())!!.cancel() assertNull(data) } @@ -316,7 +316,7 @@ class DefaultCompositePerformanceCollectorTest { collector.close() // Timer was canceled - verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer)!!.schedule(any(), any(), eq(100)) verify(fixture.mockTimer)!!.cancel() // Data was cleared From 5a05d09dfe471a997bfca442c9f65044c72dbe3a Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 10 Jun 2026 17:42:33 +0200 Subject: [PATCH 4/4] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d2849ce88..c70f025b7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,10 @@ - Improve SDK init performance by replacing `java.net.URI` with custom string parsing for DSN ([#5448](https://github.com/getsentry/sentry-java/pull/5448)) - Remove unnecessary boxing to improve performance ([#5520](https://github.com/getsentry/sentry-java/pull/5520)) -- Use fixed-delay scheduling for the performance collector to avoid catch-up collection bursts ([#5524](https://github.com/getsentry/sentry-java/pull/5524)) ### Fixes +- Fix performance collector scheduling many tasks in a row ([#5524](https://github.com/getsentry/sentry-java/pull/5524)) - Session Replay: Fix `VerifyError` in Compose masking under DexGuard/R8 obfuscation ([#5507](https://github.com/getsentry/sentry-java/pull/5507)) - Session Replay: Fix Compose view masking not working on obfuscated/minified builds ([#5503](https://github.com/getsentry/sentry-java/pull/5503))