Skip to content

perf(core): Create outbox and cache dirs lazily instead of during init (JAVA-613)#5792

Open
runningcode wants to merge 4 commits into
mainfrom
no/java-613-lazy-outbox-cache-mkdirs
Open

perf(core): Create outbox and cache dirs lazily instead of during init (JAVA-613)#5792
runningcode wants to merge 4 commits into
mainfrom
no/java-613-lazy-outbox-cache-mkdirs

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

📜 Description

Sentry.initConfigurations created the outbox and cache directories synchronously via mkdirs() on the init thread — which on Android is the main thread. This moves both off the synchronous init path by creating each directory lazily in its consumer:

  • Cache dir — created at the start of EnvelopeCache.storeInternal, so the first envelope write (on the transport thread) creates it. Covers envelopes, sessions, crash markers, and the Android ANR/tombstone markers, which all live under cacheDirPath.
  • Outbox dir — created in EnvelopeFileObserverIntegration.startOutboxSender (runs on the executor) before startWatching(), and before writing the startup-crash marker in AndroidEnvelopeCache. The native SDK already creates the outbox dir itself during sentry_init, so NDK crash writes are unaffected.

💡 Motivation and Context

Part of the "Reduce SDK init time [Android]" effort (JAVA-613). The mkdirs() stat/create calls block the caller during init; every consumer of these dirs already runs on the executor or transport thread (or, for native, creates its own dir), so the directories can be created lazily off the main thread without changing behavior.

Items 2 (context.getCacheDir() stat) and 3 (profiling-config exists()/canRead() stat) from the issue were investigated and intentionally left as-is: the former is required synchronously to set cacheDirPath and the framework already guarantees the base dir, and the latter is the persisted gate for app-start profiling that must run at ContentProvider time.

💚 How did you test it?

Added unit tests covering lazy creation for each path:

  • EnvelopeCacheTest: cache dir is created on first store when absent.
  • AndroidEnvelopeCacheTest: outbox dir is created when writing the startup-crash marker and the dir does not exist yet.
  • EnvelopeFileObserverIntegrationTest: register creates the outbox dir when it does not exist yet.
  • SentryTest: updated the three init tests to assert the dirs are no longer created during initialization.

📝 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

Verify the init-time improvement as a batch via Perfetto/macrobenchmark (individually below noise).

…t (JAVA-613)

Sentry.initConfigurations created the outbox and cache directories
synchronously on the init thread, which on Android is the main thread.

Create each directory lazily in its consumer instead: the cache dir on
the first envelope write (transport thread), and the outbox dir in the
file-observer integration (executor thread) and before writing the
startup-crash marker. The native SDK already creates the outbox dir
itself during sentry_init, so NDK crash writes are unaffected.

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

linear-code Bot commented Jul 20, 2026

Copy link
Copy Markdown

JAVA-613

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@runningcode
runningcode marked this pull request as ready for review July 20, 2026 15:05
@sentry

sentry Bot commented Jul 20, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 318.74 ms 363.02 ms 44.28 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
5b1a06b 352.27 ms 413.70 ms 61.43 ms
2124a46 319.19 ms 415.04 ms 95.85 ms
ad8da22 314.52 ms 352.47 ms 37.95 ms
d15471f 304.55 ms 408.43 ms 103.87 ms
0ee65e9 317.37 ms 366.50 ms 49.13 ms
2195398 351.77 ms 433.22 ms 81.45 ms
ee747ae 358.21 ms 389.41 ms 31.20 ms
abfcc92 309.54 ms 380.32 ms 70.78 ms
8558cac 306.16 ms 355.24 ms 49.09 ms
d15471f 310.26 ms 377.04 ms 66.78 ms

App size

Revision Plain With Sentry Diff
5b1a06b 0 B 0 B 0 B
2124a46 1.58 MiB 2.12 MiB 551.51 KiB
ad8da22 1.58 MiB 2.29 MiB 719.83 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
0ee65e9 0 B 0 B 0 B
2195398 0 B 0 B 0 B
ee747ae 1.58 MiB 2.10 MiB 530.95 KiB
abfcc92 1.58 MiB 2.13 MiB 557.31 KiB
8558cac 0 B 0 B 0 B
d15471f 1.58 MiB 2.13 MiB 559.54 KiB

Previous results on branch: no/java-613-lazy-outbox-cache-mkdirs

Startup times

Revision Plain With Sentry Diff
9b3dd56 337.72 ms 419.82 ms 82.09 ms
c32efb9 313.52 ms 358.94 ms 45.42 ms

App size

Revision Plain With Sentry Diff
9b3dd56 0 B 0 B 0 B
c32efb9 0 B 0 B 0 B

…ect (JAVA-613)

Replace the duplicated "create the dir if it does not exist" idiom in the
envelope cache, outbox file observer, and startup-crash-marker paths with a
single LazyDirectory type that materializes the directory on first access.

CacheStrategy now owns its directory as a LazyDirectory: write paths call
getOrCreate(), while path-building and validity checks use getFile() so they
do not create the directory as a side effect.

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

Copy link
Copy Markdown
Contributor Author

Native needs to be merged first: getsentry/sentry-native#1889

Have the composition roots (EnvelopeCache.create and AndroidEnvelopeCache)
build the LazyDirectory and pass it into CacheStrategy, so the cache no
longer constructs its own directory collaborator from a path string. The
public String constructor is kept and delegates, preserving binary
compatibility.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a2a855b. Configure here.

final String cacheDirPath = options.getCacheDirPath();
if (cacheDirPath != null) {
final File cacheDir = new File(cacheDirPath);
cacheDir.mkdirs();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cache send errors on missing dir

Medium Severity

Removing init-time mkdirs() for the cache dir leaves it absent on fresh installs, but SendCachedEnvelopeFireAndForgetIntegration still processes that path at startup via DirectoryProcessor. When the dir does not exist, listFiles() returns null and logs an ERROR that the cache dir is not a directory—noise on every cold start without profiling (which would incidentally create the parent via profilingTracesDir.mkdirs()). Lazy creation only happens later in EnvelopeCache.storeInternal, so the startup read path was not covered.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a2a855b. Configure here.

final @NotNull String path) {
// Materialize the outbox dir here (on the executor) so the observer can watch it for envelopes
// written by hybrid SDKs, instead of blocking Sentry.init on the mkdirs.
new LazyDirectory(path).getOrCreate();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Outbox missing after init returns

Medium Severity

Outbox creation moved onto the executor inside startOutboxSender, so after Sentry.init returns the outbox may still be missing when NDK is disabled (native no longer materializes it during init). Hybrid SDKs that write envelopes to the outbox immediately after init can fail in that window, whereas init previously created the directory synchronously before returning.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a2a855b. Configure here.

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.

1 participant