Skip to content

RUM-15191: Implement ContinuousProfilingScheduler#3298

Merged
ambushwork merged 1 commit into
feature/continuous-profilingfrom
yl/profiling/RUM-15191-continuous-profiling-scheduler
Mar 30, 2026
Merged

RUM-15191: Implement ContinuousProfilingScheduler#3298
ambushwork merged 1 commit into
feature/continuous-profilingfrom
yl/profiling/RUM-15191-continuous-profiling-scheduler

Conversation

@ambushwork

Copy link
Copy Markdown
Member

What does this PR do?

A brief description of the change being made with this pull request.

Motivation

What inspired you to submit this pull request?

Additional Notes

Anything else we should know when reviewing?

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)

@ambushwork
ambushwork force-pushed the yl/profiling/RUM-15191-continuous-profiling-scheduler branch 3 times, most recently from bf658e7 to 4a7f1ec Compare March 26, 2026 11:55
@datadog-prod-us1-3

This comment has been minimized.

@ambushwork
ambushwork force-pushed the yl/profiling/RUM-15191-continuous-profiling-scheduler branch from 4a7f1ec to 61576fe Compare March 26, 2026 12:50
@ambushwork

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: 61576fe3df

ℹ️ 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".

@codecov-commenter

codecov-commenter commented Mar 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.42105% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.58%. Comparing base (f5e2767) to head (d1b1671).

Files with missing lines Patch % Lines
...datadog/android/profiling/internal/NoOpProfiler.kt 13.64% 19 Missing ⚠️
...dog/android/profiling/internal/ProfilingFeature.kt 50.00% 4 Missing and 8 partials ⚠️
...profiling/internal/ContinuousProfilingScheduler.kt 94.55% 2 Missing and 1 partial ⚠️
...id/profiling/internal/perfetto/PerfettoProfiler.kt 81.82% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##           feature/continuous-profiling    #3298      +/-   ##
================================================================
+ Coverage                         71.47%   71.58%   +0.11%     
================================================================
  Files                               944      947       +3     
  Lines                             34873    34967      +94     
  Branches                           5912     5924      +12     
================================================================
+ Hits                              24924    25030     +106     
+ Misses                             8285     8278       -7     
+ Partials                           1664     1659       -5     
Files with missing lines Coverage Δ
...in/java/com/datadog/android/profiling/Profiling.kt 81.82% <100.00%> (ø)
...com/datadog/android/profiling/internal/Profiler.kt 100.00% <100.00%> (ø)
...id/profiling/internal/perfetto/PerfettoProfiler.kt 91.18% <81.82%> (-0.61%) ⬇️
...profiling/internal/ContinuousProfilingScheduler.kt 94.55% <94.55%> (ø)
...dog/android/profiling/internal/ProfilingFeature.kt 72.86% <50.00%> (+6.79%) ⬆️
...datadog/android/profiling/internal/NoOpProfiler.kt 13.64% <13.64%> (ø)

... and 30 files with indirect coverage changes

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

@ambushwork
ambushwork force-pushed the yl/profiling/RUM-15191-continuous-profiling-scheduler branch from 61576fe to 18512a4 Compare March 26, 2026 14:42
@ambushwork
ambushwork marked this pull request as ready for review March 26, 2026 14:55
@ambushwork
ambushwork requested review from a team as code owners March 26, 2026 14:55

@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: 18512a4d77

ℹ️ 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".

private var pendingFuture: ScheduledFuture<*>? = null

fun start(launchProfilingActive: Boolean = false) {
continuousEnabled = RateBasedSampler<Unit>(sampleRate).sample(Unit)

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.

Let's add TODO item here to make this depend on the RUM session? We anyway need to reset scheduling decision once RUM session is renewed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the RUM session sampling applies here.

So start function only launches the continuous profiling scheduler to loop the profiling/cooldown tasks, this depends on the continuous profiling sample rate. RUM session sampling rate is checked every time the active window is about to start.

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.

And this is where I'm not entirely getting it, because it means that we keep scheduling even if RUM session wasn't sampled

// If the active window is skipped due to RUM session,
// next cooldown window still needs to be scheduled.
pendingFuture = schedulerExecutor.scheduleSafe(
operationName = OPERATION_ACTIVE_WINDOW_END,
delay = activeMs,
unit = TimeUnit.MILLISECONDS,
internalLogger = internalLogger,
runnable = {
val cooldownMs = jitter(CONTINUOUS_COOLDOWN_DURATION_MS)
logToUser { LOG_ACTIVE_WINDOW_ENDED.format(Locale.US, cooldownMs) }
scheduleCooldown(cooldownMs)
}
)

I assumed that if RUM session is not sampled, we shouldn't even continue scheduling cycles/cooldowns. Is it useful to schedule them for nothing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let's defer the sampling rate change once decision is made.


fun stop() {
logToUser { LOG_STOPPED }
pendingFuture?.cancel(false)

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.

We are using non-interruptive cancel, meaning we let already running task to complete. If I'm not wrong it is not different from shutdown call below, so maybe we can use only shutdown? This will remove the need to keep the reference to the pendingFuture.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

here I keep it, since we are reusing the ScheduledExecutorService from PerfettoProfiler we should not shutdown() here, it is shut down when Profiling feature stops.

appContext = appContext,
startReason = ProfilingStartReason.CONTINUOUS,
additionalAttributes = emptyMap(),
sdkInstanceNames = setOf(sdkCore.name),

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 where we need to refactor, because instance names are needed only for the application launch profiling to perform the broadcast, but when SDK is up and running we have an access to the complete SdkCore objects. I'm not even sure we need the support of multiple SDK cores for the normal profiler runs (but we can decide on this later).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

actually providing the sdkInstanceNames here will achieve the goal that we expect, in PerfettoProfiler if it is running with a sdkInstanceName, others are rejected until it finishes.

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.

actually providing the sdkInstanceNames here will achieve the goal that we expect

yes, but my point is about something different: we rely on the SDK names rather than on SDK instances just because SDK instances are not there yet when we start profiler. It means that if we would design Profiler API for the post-SDK initialization usage, the API could be different.

Also we introduced this for the application launch use-case where we can allow multicast to the multiple SDK cores, but this doesn't really work for the continuous profiler case where profiler run must be tied to the RUM session, but since we can afford only single profiler session we can link it to only single SDK core, and we need to adapt the code to this somehow.

And now we are mixing Profiler API (and implementation) for 2 quite different cases, making the code a bit harder to follow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Create RUM-15324 to address that

Comment on lines +92 to +93
// If the active window is skipped due to RUM session,
// next cooldown window still needs to be scheduled.

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'm not sure I get it. If RUM session wasn't sampled, shouldn't we just stop all profiling activity?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think so, if this Rum session is not sampled, we should keep scheduler idling (skip the active window) until a new session is sampled in.

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.

If I get it right if RUM session is not sampled, this code will keep scheduling a loop scheduleCooldown -> scheduleNextCycle for nothing, doing no effective work rather than next cycle delay computation. Why not stop scheduling at all?

}
dataWriter = createDataWriter(sdkCore)

val scheduler = ContinuousProfilingScheduler(

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 intialize the ContinuousProfilingScheduler with a rumSessionSampled boolean? If profiling starts after RUM, is it possible to miss the initial RumSessionRenewedEvent?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it can be the case, if it happens we will need to read proactively the rum context to know if the session is tracked, I will add a todo to address it later.


// When
testedFeature.onReceive(fakeTTIDEvent)
testedFeature.onReceive(ProfilerStopEvent.TTID())

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.

seems like we can still rely on forged fakeTTIDEvent rather than on deterministic one, it should affect the test?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated

private var pendingFuture: ScheduledFuture<*>? = null

fun start(launchProfilingActive: Boolean = false) {
continuousEnabled = RateBasedSampler<Unit>(sampleRate).sample(Unit)

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.

And this is where I'm not entirely getting it, because it means that we keep scheduling even if RUM session wasn't sampled

// If the active window is skipped due to RUM session,
// next cooldown window still needs to be scheduled.
pendingFuture = schedulerExecutor.scheduleSafe(
operationName = OPERATION_ACTIVE_WINDOW_END,
delay = activeMs,
unit = TimeUnit.MILLISECONDS,
internalLogger = internalLogger,
runnable = {
val cooldownMs = jitter(CONTINUOUS_COOLDOWN_DURATION_MS)
logToUser { LOG_ACTIVE_WINDOW_ENDED.format(Locale.US, cooldownMs) }
scheduleCooldown(cooldownMs)
}
)

I assumed that if RUM session is not sampled, we shouldn't even continue scheduling cycles/cooldowns. Is it useful to schedule them for nothing?

appContext = appContext,
startReason = ProfilingStartReason.CONTINUOUS,
additionalAttributes = emptyMap(),
sdkInstanceNames = setOf(sdkCore.name),

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.

actually providing the sdkInstanceNames here will achieve the goal that we expect

yes, but my point is about something different: we rely on the SDK names rather than on SDK instances just because SDK instances are not there yet when we start profiler. It means that if we would design Profiler API for the post-SDK initialization usage, the API could be different.

Also we introduced this for the application launch use-case where we can allow multicast to the multiple SDK cores, but this doesn't really work for the continuous profiler case where profiler run must be tied to the RUM session, but since we can afford only single profiler session we can link it to only single SDK core, and we need to adapt the code to this somehow.

And now we are mixing Profiler API (and implementation) for 2 quite different cases, making the code a bit harder to follow.

Comment on lines +92 to +93
// If the active window is skipped due to RUM session,
// next cooldown window still needs to be scheduled.

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.

If I get it right if RUM session is not sampled, this code will keep scheduling a loop scheduleCooldown -> scheduleNextCycle for nothing, doing no effective work rather than next cycle delay computation. Why not stop scheduling at all?

@ambushwork
ambushwork force-pushed the yl/profiling/RUM-15191-continuous-profiling-scheduler branch 3 times, most recently from e0b1d62 to 3d8dfda Compare March 27, 2026 13:39
@ambushwork
ambushwork requested review from 0xnm and kikoveiga March 27, 2026 14:16
kikoveiga
kikoveiga previously approved these changes Mar 27, 2026
whenever(mockEditor.putStringSet(any(), any())) doReturn mockEditor
whenever(mockEditor.putFloat(any(), any())) doReturn mockEditor
testedFeature = ProfilingFeature(mockSdkCore, fakeConfiguration, mockProfiler)
fakeTTID = ProfilerStopEvent.TTID(fakeTTIDRumContext)

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.

Suggested change
fakeTTID = ProfilerStopEvent.TTID(fakeTTIDRumContext)

@ambushwork
ambushwork force-pushed the yl/profiling/RUM-15191-continuous-profiling-scheduler branch from 3d8dfda to d9f0980 Compare March 30, 2026 11:36
@ambushwork
ambushwork requested a review from 0xnm March 30, 2026 11:43
0xnm
0xnm previously approved these changes Mar 30, 2026
Comment on lines +136 to +137
verify(mockProfiler, never()).start(any(), any(), any(), any(), any())
verify(mockSchedulerExecutor, never()).schedule(any<Runnable>(), any(), any())

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'm not sure, but probably the safer assert here would be verifyNoInteractions, because this test may pass if some other similar methods are actually called with slightly different method signature.

Same for similar lines in the tests below.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

only mockSchedulerExecutor is updated here, the other assertions on mockProfiler are kept since there are some other interactions on it.

@ambushwork
ambushwork force-pushed the yl/profiling/RUM-15191-continuous-profiling-scheduler branch from d9f0980 to d1b1671 Compare March 30, 2026 13:05
@ambushwork
ambushwork requested review from 0xnm and kikoveiga March 30, 2026 13:46
@ambushwork
ambushwork merged commit ce821f2 into feature/continuous-profiling Mar 30, 2026
26 checks passed
@ambushwork
ambushwork deleted the yl/profiling/RUM-15191-continuous-profiling-scheduler branch March 30, 2026 14:15
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