Skip to content

Commit

Permalink
RUM-6093:Add TimeBank in session replay recorder for dynamic optimisa…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
ambushwork committed Sep 9, 2024
1 parent 679de59 commit fe0410a
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ package com.datadog.android.sessionreplay.internal.recorder

import android.os.Handler
import android.os.Looper
import com.datadog.android.api.InternalLogger
import java.util.concurrent.TimeUnit

internal class Debouncer(
private val handler: Handler = Handler(Looper.getMainLooper()),
private val maxRecordDelayInNs: Long = MAX_DELAY_THRESHOLD_NS
private val maxRecordDelayInNs: Long = MAX_DELAY_THRESHOLD_NS,
private val timeBank: TimeBank = RecordingTimeBank(),
private val internalLogger: InternalLogger
) {

private var lastTimeRecordWasPerformed = 0L
Expand All @@ -37,8 +40,28 @@ internal class Debouncer(
}

private fun executeRunnable(runnable: Runnable) {
runnable.run()
lastTimeRecordWasPerformed = System.nanoTime()
runInTimeBalance {
runnable.run()
lastTimeRecordWasPerformed = System.nanoTime()
}
}

private fun runInTimeBalance(block: () -> Unit) {
if (timeBank.updateAndCheck(System.nanoTime())) {
val startTimeInNano = System.nanoTime()
block()
val endTimeInNano = System.nanoTime()
timeBank.consume(endTimeInNano - startTimeInNano)
} else {
// TODO: RUM-5188 report with telemetry about the frames skipped
internalLogger.log(
InternalLogger.Level.INFO,
InternalLogger.Target.MAINTAINER,
messageBuilder = {
MSG_FRAME_SKIP
}
)
}
}

companion object {
Expand All @@ -47,5 +70,7 @@ internal class Debouncer(

// one frame time
internal const val DEBOUNCE_TIME_IN_MS: Long = 64

private const val MSG_FRAME_SKIP = "Session Replay skipped this recording due to the time limit"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.internal.recorder

import java.util.concurrent.TimeUnit
import kotlin.math.min

/**
* Implementation of [TimeBank], it maintains a time balance which can be consumed for session replay recording.
* Every time the session replay recording task tries to execute, [updateAndCheck] is called to increase the time
* balance according to the time passed since last call, and check if the balance is positive to execute the task.
*/
internal class RecordingTimeBank(
private val maxTimeBalancePerSecondInMs: Long = DEFAULT_MAX_TIME_BALANCE_PER_SEC_IN_MS
) : TimeBank {

@Volatile
private var recordingTimeBalanceInNano = TimeUnit.MILLISECONDS.toNanos(maxTimeBalancePerSecondInMs)

@Volatile
private var lastStartTime: Long = 0

override fun consume(executionTime: Long) {
recordingTimeBalanceInNano -= executionTime
}

override fun updateAndCheck(timestamp: Long): Boolean {
increaseTimeBank(timestamp)
lastStartTime = timestamp
return recordingTimeBalanceInNano >= 0
}

private fun increaseTimeBank(timestamp: Long) {
val timePassedSinceLastExecution = timestamp - lastStartTime
// The factor of balance increasing by time. if increasing 100ms balance in the bank takes 1000ms, then the
// factor will be 100ms/1000ms = 0.1f
val balanceFactor = maxTimeBalancePerSecondInMs.toFloat() / TimeUnit.SECONDS.toMillis(1)
recordingTimeBalanceInNano += (timePassedSinceLastExecution * balanceFactor).toLong()
recordingTimeBalanceInNano =
min(TimeUnit.MILLISECONDS.toNanos(maxTimeBalancePerSecondInMs), recordingTimeBalanceInNano)
}

companion object {
private const val DEFAULT_MAX_TIME_BALANCE_PER_SEC_IN_MS = 100L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.internal.recorder

import com.datadog.tools.annotation.NoOpImplementation

@NoOpImplementation
internal interface TimeBank {

/**
* Called to consume execution time from the bank.
*/
fun consume(executionTime: Long)

/**
* Called to update time bank balance and check if the given timestamp
* is allowed according to the current time balance.
*
* @return true if the given timestamp is allowed by time bank to execute a task, false otherwise.
*/
fun updateAndCheck(timestamp: Long): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ internal class WindowsOnDrawListener(
private val snapshotProducer: SnapshotProducer,
private val privacy: SessionReplayPrivacy,
private val imagePrivacy: ImagePrivacy,
private val debouncer: Debouncer = Debouncer(),
private val miscUtils: MiscUtils = MiscUtils,
private val internalLogger: InternalLogger,
private val debouncer: Debouncer = Debouncer(internalLogger = internalLogger),
private val methodCallSamplingRate: Float
) : ViewTreeObserver.OnDrawListener {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.datadog.android.sessionreplay.internal.recorder

import android.os.Handler
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
Expand Down Expand Up @@ -38,11 +39,18 @@ internal class DebouncerTest {
@Mock
lateinit var mockHandler: Handler

@Mock
lateinit var mockInternalLogger: InternalLogger

lateinit var testedDebouncer: Debouncer

@BeforeEach
fun `set up`() {
testedDebouncer = Debouncer(mockHandler, TEST_MAX_DELAY_THRESHOLD_IN_NS)
testedDebouncer = Debouncer(
mockHandler,
TEST_MAX_DELAY_THRESHOLD_IN_NS,
internalLogger = mockInternalLogger
)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.recorder

import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import com.datadog.android.sessionreplay.internal.recorder.RecordingTimeBank
import com.datadog.android.sessionreplay.internal.recorder.TimeBank
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.quality.Strictness
import java.util.concurrent.TimeUnit

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(ForgeConfigurator::class)
class RecordingTimeBankTest {

private lateinit var recordingTimeBank: TimeBank

@BeforeEach
fun `set up`() {
recordingTimeBank = RecordingTimeBank(TEST_MAX_BALANCE_IN_MS)
}

@Test
fun `M allow the first execution W check`(forge: Forge) {
// Given
val timestamp = forge.aLong(min = 0)

// When
val actual = recordingTimeBank.updateAndCheck(timestamp)

// Then
assertThat(actual).isEqualTo(true)
}

@Test
fun `M skip the next execution W previous consume out the balance`(forge: Forge) {
// Given
val firstTimestamp = forge.aLong(min = 0)
val firstExecutionTime = forge.aLong(
min = TimeUnit.MILLISECONDS.toNanos(TEST_MAX_BALANCE_IN_MS),
max = TimeUnit.MILLISECONDS.toNanos(TEST_MAX_BALANCE_IN_MS) * 100
)
val interval = forge.aLong(min = 0, max = firstExecutionTime)
val secondTimestamp = forge.aLong(
min = firstTimestamp + firstExecutionTime,
max = firstTimestamp + firstExecutionTime + interval
)

// When
recordingTimeBank.updateAndCheck(firstTimestamp)
recordingTimeBank.consume(firstExecutionTime)
val actual = recordingTimeBank.updateAndCheck(secondTimestamp)

// Then
assertThat(actual).isEqualTo(false)
}

@Test
fun `M allow the next execution W balance is recovery`(forge: Forge) {
// Given
val firstTimestamp = forge.aLong(min = 0)
val firstExecutionTime = forge.aLong(
min = TimeUnit.MILLISECONDS.toNanos(TEST_MAX_BALANCE_IN_MS),
max = TimeUnit.MILLISECONDS.toNanos(TEST_MAX_BALANCE_IN_MS) * 100
)
val interval = forge.aLong(min = 0, max = firstExecutionTime)
val secondTimestamp = forge.aLong(
min = firstTimestamp + firstExecutionTime,
max = firstTimestamp + firstExecutionTime + interval
)

val thirdTimestamp =
forge.aLong(
min = secondTimestamp + firstExecutionTime /
((TimeUnit.SECONDS.toMillis(1) / TEST_MAX_BALANCE_IN_MS))
)
// When
recordingTimeBank.updateAndCheck(firstTimestamp)
recordingTimeBank.consume(firstExecutionTime)
recordingTimeBank.updateAndCheck(secondTimestamp)
val actual = recordingTimeBank.updateAndCheck(thirdTimestamp)

// Then
assertThat(actual).isEqualTo(true)
//
}

@Test
fun `M allow everything W set max balance to 1000ms per sec`(forge: Forge) {
recordingTimeBank = RecordingTimeBank(1000)

// Given
val firstTimestamp = forge.aLong(min = 0)
val firstExecutionTime = forge.aLong(
min = TimeUnit.MILLISECONDS.toNanos(TEST_MAX_BALANCE_IN_MS),
max = TimeUnit.MILLISECONDS.toNanos(TEST_MAX_BALANCE_IN_MS) * 100
)
val interval = forge.aLong(min = 0, max = firstExecutionTime)
val secondTimestamp = forge.aLong(
min = firstTimestamp + firstExecutionTime,
max = firstTimestamp + firstExecutionTime + interval
)

// When
recordingTimeBank.updateAndCheck(firstTimestamp)
recordingTimeBank.consume(firstExecutionTime)
val actual = recordingTimeBank.updateAndCheck(secondTimestamp)

// Then
assertThat(actual).isEqualTo(true)
}

companion object {
private const val TEST_MAX_BALANCE_IN_MS = 100L
}
}

0 comments on commit fe0410a

Please sign in to comment.