Skip to content

PANA-8612: Add snapshot capture orchestration - #3722

Open
jonathanmos wants to merge 4 commits into
feature/sr-new-pipelinefrom
jmoskovich/02-sr-new-pipeline
Open

jonathanmos wants to merge 4 commits into
feature/sr-new-pipelinefrom
jmoskovich/02-sr-new-pipeline

Conversation

@jonathanmos

@jonathanmos jonathanmos commented Aug 18, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Second PR in the new composition pipeline series, stacked on PANA-8500.

Adds the orchestration layer: SnapshotCaptureOrchestrator serializes snapshot "generations"
end-to-end (traversal, async processing, expiry, handoff), coalescing overlapping requests into one
follow-up and accumulating a CaptureChangeset of what changed (accepted, not yet consumed by
traversal). A new DefaultCompositionPipelineFactory assembles it and its collaborators, leaving
DefaultRecorderProvider with pipeline selection only.

Also in scope:

  • Heatmaps and embedded content are silently dropped by this pipeline (embedded content was
    descoped in PANA-8500). Both now log WARN/Target.USER, and setCompositionTreeRecordingEnabled's
    KDoc documents them.
  • The completion queue is bounded (tentatively 4, subject to change once snapshot size is known),
    dropping and expiring the oldest capture on saturation, and now expires captures it refuses so no
    generation stays active holding a snapshot.
  • ActiveWindowSource and CompositionViewOnDrawInterceptor are reachable from every window's draw
    callbacks, so their mutable state is guarded rather than assumed main-thread confined.

Motivation

The recorder and processing pieces already existed; this adds the piece that drives a capture
end-to-end. The real view-traversal producer is PANA-8613, so the producer here is
NoOpCapturedSnapshotProducer and compositionTreeRecordingEnabled stays disabled by default
behind @ExperimentalSessionReplayApi.

Additional Notes

  • CompositionCaptureConcurrencyTest covers the same-structure races by hammering and the ordering
    ones by forced interleaving; each test was verified to fail with its guard reverted.
  • No performance data yet — the 64ms coalescing delay and 90ms generation budget are unvalidated.
    Tuning belongs to PANA-8617.
  • Unit-level coverage only; no instrumented test of the interception/lifecycle wiring yet.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@datadog-official

datadog-official Bot commented Aug 18, 2026

Copy link
Copy Markdown

Tests

All CI checks and tests passed. Datadog automation helped this PR pass.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 1 test - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 86.10%
Overall Coverage: 71.48% (+0.35%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 357bf07 | Docs | View more details | Give us feedback!

@codecov-commenter

codecov-commenter commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.80127% with 105 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.35%. Comparing base (2ebd855) to head (50400d0).
⚠️ Report is 1 commits behind head on feature/sr-new-pipeline.

Files with missing lines Patch % Lines
...nternal/composition/SnapshotCaptureOrchestrator.kt 80.74% 10 Missing and 16 partials ⚠️
...ay/internal/composition/SnapshotCompletionQueue.kt 63.08% 13 Missing and 11 partials ⚠️
...lay/internal/composition/CompositionDrawSignals.kt 64.58% 12 Missing and 5 partials ⚠️
...ay/internal/composition/AndroidCaptureExecution.kt 70.00% 4 Missing and 5 partials ⚠️
.../sessionreplay/internal/DefaultRecorderProvider.kt 84.91% 6 Missing and 2 partials ⚠️
...y/internal/composition/CaptureGenerationContext.kt 87.27% 3 Missing and 4 partials ⚠️
...ionreplay/internal/composition/CaptureWorkToken.kt 75.00% 3 Missing and 2 partials ⚠️
...nal/composition/AndroidSnapshotCaptureLifecycle.kt 82.61% 3 Missing and 1 partial ⚠️
...onreplay/internal/composition/CaptureTimeBudget.kt 75.00% 1 Missing and 1 partial ⚠️
...nreplay/internal/composition/SnapshotProcessing.kt 86.67% 2 Missing ⚠️
... and 1 more
Additional details and impacted files
@@                     Coverage Diff                     @@
##           feature/sr-new-pipeline    #3722      +/-   ##
===========================================================
+ Coverage                    73.30%   73.35%   +0.04%     
===========================================================
  Files                         1009     1021      +12     
  Lines                        37438    37902     +464     
  Branches                      6515     6599      +84     
===========================================================
+ Hits                         27443    27800     +357     
- Misses                        8116     8183      +67     
- Partials                      1879     1919      +40     
Files with missing lines Coverage Δ
...ndroid/sessionreplay/SessionReplayConfiguration.kt 97.89% <ø> (-0.02%) ⬇️
...lay/internal/composition/CancellableCaptureWork.kt 100.00% <100.00%> (ø)
...nternal/composition/CaptureSkippedFrameNotifier.kt 100.00% <100.00%> (ø)
...sessionreplay/internal/composition/CapturedTree.kt 90.24% <100.00%> (+0.24%) ⬆️
...internal/composition/CompositionCapturePipeline.kt 100.00% <100.00%> (+54.55%) ⬆️
...onreplay/internal/composition/CaptureScheduling.kt 0.00% <0.00%> (ø)
...onreplay/internal/composition/CaptureTimeBudget.kt 75.00% <75.00%> (ø)
...nreplay/internal/composition/SnapshotProcessing.kt 86.67% <86.67%> (ø)
...nal/composition/AndroidSnapshotCaptureLifecycle.kt 82.61% <82.61%> (ø)
...ionreplay/internal/composition/CaptureWorkToken.kt 75.00% <75.00%> (ø)
... and 6 more

... and 34 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 001809f to 1ca510f Compare August 18, 2026 10:38
Base automatically changed from jmoskovich/01-sr-new-pipeline to feature/sr-new-pipeline August 18, 2026 12:16
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch 2 times, most recently from 99626d9 to 1dd5cfc Compare August 19, 2026 09:40
@jonathanmos

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1dd5cfca85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 1dd5cfc to 50400d0 Compare August 19, 2026 10:38
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch 3 times, most recently from e59a111 to 6d73790 Compare September 2, 2026 11:08
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch 2 times, most recently from 06dc1f8 to 783469c Compare September 2, 2026 13:59
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 783469c to 902b1be Compare September 6, 2026 20:44
@jonathanmos
jonathanmos marked this pull request as ready for review September 6, 2026 21:21
@jonathanmos
jonathanmos requested review from a team as code owners September 6, 2026 21:21
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-07T12:04:25.688272Z 357bf07 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 902b1bee2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sbarrio
sbarrio requested a review from satween September 7, 2026 07:05

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab03b99b6a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 11c8119e7b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 11c8119 to 13a4a1d Compare September 7, 2026 10:06

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13a4a1d192

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 13a4a1d to a21f246 Compare September 7, 2026 10:54

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a21f246719

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from a21f246 to 357bf07 Compare September 7, 2026 11:57
*
* Disabled by default.
*
* Heatmap recording ([setHeatmapsEnabled]) is not supported by this pipeline yet; combining

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can either remove this new part or add a todo here in case we forget it here in the future.


import com.datadog.android.internal.time.TimeProvider

internal class TimeProviderCaptureTimeProvider(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This class name seems weird to me, should it be simpler?

internal class TimeProviderCaptureTimeProvider(
private val timeProvider: TimeProvider
) : CaptureTimeProvider {
override fun elapsedRealtimeNanos(): Long = timeProvider.getDeviceElapsedTimeNanos()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not sure I understand the meaning of this wrapper class, why can't we just use the timeProvier and call getDeviceElapsedTimeNanos directly?

* Deadline/cancellation are checked on both sides of the adapter call, and only the time spent
* actively executing [block] is charged to the recording time bank.
*/
fun <T> runMainThreadCaptureUnit(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe annotate this with @MainThread?

private val sdkCore: FeatureSdkCore
) {
fun notifySkippedFrame() {
sdkCore.getFeature(Feature.RUM_FEATURE_NAME)?.sendEvent(SKIPPED_FRAME_EVENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

By curiosity how RUM will consume this event?

internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.TELEMETRY,
{ "Unable to $operation composition onDrawListener on viewTreeObserver" },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

in which exact case the IllegalStateException will throw, is this actionable as a telemetry?

flushPositionBufferThresholdInNs = flushPositionBufferThresholdInNs
)

@MainThread

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is annotated with @MainThread to keep isActive thread-safe?

return try {
super.dispatchTouchEvent(event)
} catch (e: NullPointerException) {
logOrRethrowWrappedCallbackException(e)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we have to log the exception here since we've null check above and we can return earlier?

@hamorillo hamorillo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I did a first high-level pass through the production files and didn't notice any major architectural issues. I couldn't review everything in depth, so I relied heavily on AI assistance for parts of this.

I think the PR is too big, which makes a deep review difficult.

I'd suggest splitting this kind of work into several PRs going forward — for example, the file reorganization in CapturedTree, CapturedTreeValidator, and CapturedTreeWireMapper is unrelated to the orchestration logic and could have been its own PR. Touch interception (CompositionWindowTouchCallback/CompositionWindowTouchInterceptor) is also fully independent of the snapshot orchestration and could have been split out on its own, leaving the orchestrator/queue work as a tighter, more focused PR.

There are some files adding code that is currently never used in production, only in tests (example: DefaultCapturedIdentityFactory). I wonder if those are also susceptible to being split.

I'm not saying I won't approve this PR once the comments are solved, just sharing some thoughts.

Comment on lines +30 to +33
private fun Long.toDelayMillis(): Long {
val delayMs = TimeUnit.NANOSECONDS.toMillis(this)
return if (delayMs < 1L) 1L else delayMs
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit:

Suggested change
private fun Long.toDelayMillis(): Long {
val delayMs = TimeUnit.NANOSECONDS.toMillis(this)
return if (delayMs < 1L) 1L else delayMs
}
private fun Long.toDelayMillis(): Long =
TimeUnit.NANOSECONDS.toMillis(this).coerceAtLeast(1L)


internal class DefaultCapturedIdentityFactory(
override val scope: RumViewIdentityScope,
private val replayIdGenerator: CapturedReplayIdGenerator = AutoIncrementingCapturedReplayIdGenerator(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 The file-split changed this constructor's default from a shared companion-object generator to a fresh per-instance one:

// before
private val replayIdGenerator: CapturedReplayIdGenerator = SHARED_REPLAY_ID_GENERATOR
// now
private val replayIdGenerator: CapturedReplayIdGenerator = AutoIncrementingCapturedReplayIdGenerator()

Each factory instance now generates IDs starting from 0 instead of sharing one counter. No impact today since nothing constructs this factory yet, but if that ever changes without one shared generator being injected across instances, wireframe IDs will collide across windows/views. Intentional, or should the shared default be restored?


when (val mapping = wireMapper.mapFullSnapshot(capture.snapshot)) {
is CaptureWireMappingResult.Success -> {
if (capture.generation.tryAccept()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

❓ What happen is tryAccept return false? Should we handle that case?

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.

4 participants