Skip to content

[Android] Add Pre-Init memory buffer - #1208

Draft
FranAguilera wants to merge 2 commits into
mainfrom
franjam/add-pre-init-jvm-buffer
Draft

FranAguilera wants to merge 2 commits into
mainfrom
franjam/add-pre-init-jvm-buffer

Conversation

@FranAguilera

@FranAguilera FranAguilera commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Goal

Resolves BIT-9500

Adding a PreInit in memory buffer for the logger calls that occur while the SDK is starting internally.

This is the current flow in prod

Screenshot 2026-09-16 at 17 44 25

And here with the proposal

Screenshot 2026-09-16 at 17 44 00

The implementation does:

  • PreInitInMemoryLogger is added only when Capture state is LoggerState.Starting so all Capture.Logger.* calls are forwarded there
  • The instance is removed once we reach the Started/StartFailure state
  • Timestamps for each log are respected
  • To guard against potential OOM if starts is stuck, when the in memory cached calls reached a limit of 512 KB the new events will be dropped and an internal log will be emitted

Verification

Steps:

  • In the gradle-test-app, go to setting and enable, Start with simulated Delay and Start SDK on Background Thread
Screenshot 2026-09-17 at 15 33 28
  • Restart the app, go to SDK tab and trigger many calls (e.g. Log Many messages) etc
Screenshot 2026-09-17 at 15 34 27
  • The internal initialization priorly would have miss those logs but now they are added into the buffer and replaying later. See session

  • CHANGELOG.md's "Unreleased" section has been updated, if applicable.

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown

Android Size Comparison Report (x86_64)

Metric APK (KB) SO (KB)
Baseline 3504 1378
Current 3508 1378
Difference 4 0

APK size increased by 4 KB. SO size unchanged.

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown

Android Benchmark Results

Device: PR: Emulator - Android SDK built for x86_64 (API 31) | main: Emulator - Android SDK built for x86_64 (API 31)

Note: Benchmarks run on emulator. Results may vary between runs and may differ from physical devices.

Allocations

Test PR main Δ
clearEntityId 0 0 0.0%
getSdkStatus 1 1 0.0%
logAppLaunchTTI 0 0 0.0%
logHttpNetworkLog50FieldsAndHeadersAndInitialFields 850 850 0.0%
logNotMatched5000Fields 10 10 0.0%
logNotMatchedNoFields 0 0 0.0%
loggerImplCreation 495 491 +0.8%
parseTreeWithOverlay 298 298 0.0%
parseTreeWithoutOverlay 298 298 0.0%
processJvmCrash 2253 2253 0.0%
processNativeCrash 2943 2943 0.0%
setEntityId 0 0 0.0%
startNewSession 0 0 0.0%
trackSpansWithFields 65 65 0.0%
trackSpansWithoutFields 45 45 0.0%
webViewBridgeBridgeReady 52 52 0.0%
webViewBridgeInvalidJson 38 38 0.0%

Timing

Test PR main Δ
clearEntityId 89 ns 85 ns +5.0%
getSdkStatus 189 ns 176 ns +7.2%
logAppLaunchTTI 90 ns 83 ns +9.4%
logHttpNetworkLog50FieldsAndHeadersAndInitialFields 215.76 us 200.58 us +7.6%
logNotMatched5000Fields 2.40 ms 2.20 ms +9.0%
logNotMatchedNoFields 724 ns 656 ns +10.5%
loggerImplCreation 1.17 ms 1.19 ms -1.6%
parseTreeWithOverlay 110.71 us 105.93 us +4.5%
parseTreeWithoutOverlay 113.05 us 106.05 us +6.6%
processJvmCrash 480.15 us 459.87 us +4.4%
processNativeCrash 422.18 us 403.37 us +4.7%
setEntityId 310 ns 281 ns +10.1%
startNewSession 3.17 us 2.11 us +50.2%
trackSpansWithFields 454.42 us 417.54 us +8.8%
trackSpansWithoutFields 8.03 us 7.61 us +5.6%
webViewBridgeBridgeReady 8.25 us 7.85 us +5.1%
webViewBridgeInvalidJson 147.26 us 148.81 us -1.0%

@FranAguilera
FranAguilera force-pushed the franjam/add-pre-init-jvm-buffer branch 5 times, most recently from 6fbb1e0 to 6ca9807 Compare September 16, 2026 15:40
@FranAguilera
FranAguilera marked this pull request as ready for review September 16, 2026 15:52
@FranAguilera
FranAguilera requested a balanced review from Copilot September 16, 2026 15:54
@JvmStatic
val sessionUrl: String?
get() = logger()?.sessionUrl
get() = logger()?.takeUnless { it is PreInitInMemoryLogger }?.sessionUrl

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ignores the pre init buffer value as we are not initialized. Also choose to not break the non-nullable contract on the ILogger api

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this means that we'll break anyone doing

Capture.start()
Capture.sessionUrl() 

yeah?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i think most folks may be checking nullability to infer valid session url, so here there is no much change from that logic to keep this to be non-null until sdk is fully started

One thing we could do is to mark those (Capture.Logger.sessionUrl, Capture.Logger.sessionId, etc) as deprecated and direct people from the deprecation note to rely on the existing StartResult callback that returns an object with the non-nullable sessionId,url, etc when there is a successful start

Screenshot 2026-09-18 at 17 04 26

wdyt?

@FranAguilera
FranAguilera force-pushed the franjam/add-pre-init-jvm-buffer branch from 6ca9807 to 5d746a5 Compare September 16, 2026 15:58

This comment was marked as outdated.

@FranAguilera
FranAguilera force-pushed the franjam/add-pre-init-jvm-buffer branch 3 times, most recently from 109ba17 to 769d6d2 Compare September 17, 2026 13:31
@FranAguilera FranAguilera reopened this Sep 17, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 17, 2026
@FranAguilera
FranAguilera requested a balanced review from Copilot September 17, 2026 13:47
@FranAguilera
FranAguilera force-pushed the franjam/add-pre-init-jvm-buffer branch 4 times, most recently from 1a47142 to 8d95674 Compare September 17, 2026 14:27
@FranAguilera
FranAguilera force-pushed the franjam/add-pre-init-jvm-buffer branch from 5503734 to fc5ac26 Compare September 17, 2026 14:37
@FranAguilera
FranAguilera requested a review from snowp September 17, 2026 15:33
@bitdriftlabs bitdriftlabs unlocked this conversation Sep 17, 2026

@snowp snowp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mostly makes sense to me, a few points and a few possible inconsistencies I spotted. We should decide whether we are okay with where this approach will get things slightly wrong in some cases or if we need to put in more effort for some of the edge cases

* Attaches the real logger to a span created before one existed yet (i.e. while the SDK was
* still starting), emitting its deferred start log and letting a subsequent end() call work.
*/
internal fun attachLogger(logger: IInternalLogger) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would we want some kind of defensive programming here to avoid duplicate emission? Like a deferred flag or something that tells us whether we need to do this? The coordination here seems pretty loose

@JvmStatic
val sessionUrl: String?
get() = logger()?.sessionUrl
get() = logger()?.takeUnless { it is PreInitInMemoryLogger }?.sessionUrl

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this means that we'll break anyone doing

Capture.start()
Capture.sessionUrl() 

yeah?

logger()?.also {
it.createTemporaryDeviceCode {
mainThreadHandler.run { completion(it) }
logger()?.takeUnless { it is PreInitInMemoryLogger }?.also { logger ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we just have something like initalizedLogger() to avoid this predicate check in multiple places?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good call, will add it

Comment on lines +925 to 929
val flushToNativeDuration =
measureTime {
preInitInMemoryLogger.flushToNative(loggerImpl)
}
default.set(LoggerState.Started(loggerImpl))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is technically racy as we could get inbound logs after flushToNative ends and default.set is called? I think we'd need a lock or other work to make this better if we care about this narrow log drop chance


startResult.invokeCatchingOrThrowOnDebug(CaptureResult.Success(loggerImpl))
} catch (throwable: Throwable) {
preInitInMemoryLogger.clear()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this necessary? won't it be deallocated as part of updating default? Seems fine but not sure if we need it

If something can retain a handle to it then I imagine we'd want to make it drop inbounds as well if we wanted to avoid it doing all the log insertion and accounting

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good point, will clean up

Comment on lines +65 to +68
override fun setFeatureFlagExposure(
name: String,
variant: String,
) = add(BufferedCall.StringFeatureFlagExposure(name, variant))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

technically this ought to also capture the timestamp but maybe not worth the complexity to add

) : BufferedCall {
override val sizeBytes = sized(request.toString())

override fun replay(logger: IInternalLogger) = logger.log(request)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

wouldn't this also need a timestamp override? or is that captured in the request info? same for the other convenience functions

@FranAguilera
FranAguilera marked this pull request as draft September 18, 2026 15:22
@FranAguilera

Copy link
Copy Markdown
Contributor Author

Moving to draft until all feedback is addressed

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.

3 participants