Skip to content

fix(clientreport): Stop deserializing discarded logs (JAVA-662)#5835

Draft
runningcode wants to merge 2 commits into
mainfrom
no/java-662-cpu-busy-loop
Draft

fix(clientreport): Stop deserializing discarded logs (JAVA-662)#5835
runningcode wants to merge 2 commits into
mainfrom
no/java-662-cpu-busy-loop

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

📜 Description

ClientReportRecorder.recordLostEnvelopeItem counted discarded log and metric items by fully deserializing the envelope payload just to read getItems().size(). The item count is already carried in the envelope item header (item_count, set by SentryEnvelopeItem.fromLogs/fromMetrics and round-tripped through the disk cache), so this change reads it from the header instead of deserializing. Byte counts still come from the raw getData() bytes.

  • Added SentryEnvelopeItemHeader.getItemCount().
  • Log/metric branches of recordLostEnvelopeItem now use the header count; when it is absent (legacy/foreign envelopes) they fall back to 1 and never deserialize.
  • Transactions are untouched — span counts are not stored in the header and that path isn't implicated.

💡 Motivation and Context

Under sustained rate limiting (e.g. transaction/span quota exhausted while logs.enabled=true), envelopes are discarded continuously via rate limiting → queue/cache overflow → send/network errors. Each discard ran a full JSON deserialization on the SentryAsyncConnection/SentryExecutorService threads. The gson reader's error-tolerant recovery machinery throws an exception per token, and Throwable.fillInStackTrace dominated the CPU profile — the cost then fed back into more queue overflow and more discards, pinning CPU cores in a busy-loop indefinitely. Delivery of non-rate-limited categories (errors) stopped, and the only workaround was disabling logs.

Reading the count from the header makes the discard path O(1) and allocation/exception-free.

Fixes GH-5830
Fixes JAVA-662

💚 How did you test it?

Unit tests in ClientReportTest:

  • Existing log/metric count tests remain green (header count equals list size).
  • New regression test: a log item whose payload would fail to deserialize is still counted from the header, and getLogs() is verified never to be called.
  • New test for the null item_count fallback (counts as 1).

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

None.

ClientReportRecorder counted discarded log and metric items by fully
deserializing the envelope payload just to read its size. On the discard
path this runs continuously under sustained rate limiting, and the JSON
reader's error-tolerant recovery throws an exception per token, pinning
CPU cores in a busy-loop (fillInStackTrace dominated the profile).

The item count is already stored in the envelope item header, so read it
from there instead of deserializing. Byte counts still come from the raw
data. This makes the discard path O(1) and allocation/exception-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 24, 2026

Copy link
Copy Markdown

JAVA-662

@sentry

sentry Bot commented Jul 24, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.50.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 298.98 ms 374.02 ms 75.04 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
22f4345 307.87 ms 354.51 ms 46.64 ms
8c7718c 307.42 ms 374.84 ms 67.42 ms
bb0ff41 321.00 ms 378.28 ms 57.28 ms
c8125f3 397.65 ms 485.14 ms 87.49 ms
eb95ded 317.51 ms 369.08 ms 51.57 ms
f634d01 375.06 ms 420.04 ms 44.98 ms
9fbb112 359.71 ms 421.85 ms 62.14 ms
d501a7e 307.33 ms 341.94 ms 34.61 ms
bb0ff41 317.76 ms 384.66 ms 66.90 ms
9054d65 330.94 ms 403.24 ms 72.30 ms

App size

Revision Plain With Sentry Diff
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
8c7718c 0 B 0 B 0 B
bb0ff41 0 B 0 B 0 B
c8125f3 1.58 MiB 2.10 MiB 532.32 KiB
eb95ded 0 B 0 B 0 B
f634d01 1.58 MiB 2.10 MiB 533.40 KiB
9fbb112 1.58 MiB 2.11 MiB 539.18 KiB
d501a7e 0 B 0 B 0 B
bb0ff41 0 B 0 B 0 B
9054d65 1.58 MiB 2.29 MiB 723.38 KiB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CPU busy-loop in ClientReportRecorder.recordLostEnvelope → SentryEnvelopeItem.getLogs() when envelopes are discarded under rate limiting (8.49.0)

1 participant