-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2247 from DataDog/yl/sr-opt/cpu-bank
RUM-6093:Add TimeBank in session replay recorder for dynamic optimisation
- Loading branch information
Showing
6 changed files
with
252 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
.../src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/RecordingTimeBank.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/* | ||
* 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 | ||
|
||
/** | ||
* Time Bank is a concept representing an allocated execution quota per second. For example, if the quota is set to | ||
* 100 milliseconds per second, it means that within any given second, no more than 100 milliseconds can be used for | ||
* executing operations. If the full quota of 100 milliseconds has already been used within a second, further execution | ||
* is not permitted until the next second begins and the quota is recharged. Conversely, if less than 100 milliseconds | ||
* has been used and the second has not yet elapsed, execution may continue until the quota is reached. | ||
*/ | ||
internal class RecordingTimeBank( | ||
private val maxTimeBalancePerSecondInMs: Long = DEFAULT_MAX_TIME_BALANCE_PER_SEC_IN_MS | ||
) : TimeBank { | ||
|
||
// The normalized factor of balance increasing by time. If increasing 100ms balance in the bank takes 1000ms, | ||
// then the factor will be 100ms/1000ms = 0.1f | ||
private val balanceFactor = maxTimeBalancePerSecondInMs.toDouble() / TimeUnit.SECONDS.toMillis(1) | ||
|
||
@Volatile | ||
private var recordingTimeBalanceInNano = TimeUnit.MILLISECONDS.toNanos(maxTimeBalancePerSecondInMs) | ||
|
||
@Volatile | ||
private var lastCheckTime: Long = 0 | ||
|
||
override fun consume(executionTime: Long) { | ||
recordingTimeBalanceInNano -= executionTime | ||
} | ||
|
||
override fun updateAndCheck(timestamp: Long): Boolean { | ||
increaseTimeBank(timestamp) | ||
lastCheckTime = timestamp | ||
return recordingTimeBalanceInNano >= 0 | ||
} | ||
|
||
private fun increaseTimeBank(timestamp: Long) { | ||
val timePassedSinceLastExecution = timestamp - lastCheckTime | ||
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 | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
...on-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TimeBank.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
...eplay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/RecordingTimeBankTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* 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).isTrue() | ||
} | ||
|
||
@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).isFalse() | ||
} | ||
|
||
@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) | ||
check(!recordingTimeBank.updateAndCheck(secondTimestamp)) | ||
val actual = recordingTimeBank.updateAndCheck(thirdTimestamp) | ||
|
||
// Then | ||
assertThat(actual).isTrue() | ||
} | ||
|
||
@Test | ||
fun `M allow everything W set max balance more than 1000ms per sec`(forge: Forge) { | ||
val maxBalancePerSecondInMs = forge.aLong(min = 1000) | ||
recordingTimeBank = RecordingTimeBank(maxBalancePerSecondInMs) | ||
|
||
// 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 | ||
} | ||
} |