Created
June 20, 2025 23:19
-
-
Save kaushikgopal/4adcd72aa253179340126391339ce2e2 to your computer and use it in GitHub Desktop.
Samplers in Kotlin
This file contains hidden or 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
/** | |
* runs a block at most [rate] times per [period] | |
* | |
* Examples: | |
* - [rate=30 period=1s]: run max 30 times within period of 1 second (30fps) | |
* - [rate=10 period=1mt]: run max 10 times within period of 1 minuts (10 updates per minute) | |
* | |
* @param rate Maximum number of executions per period | |
* @param period Time duration for the rate limit | |
* @param timeSource Source for time measurements (default: monotonic system time) | |
*/ | |
class TimeSampler( | |
val rate: Int, | |
val period: Duration = 1.seconds, | |
val timeSource: TimeSource = TimeSource.Monotonic, | |
) { | |
private val minInterval: Duration = period / rate | |
private var lastExecutionMark: TimeMark? = null | |
/** | |
* execute [block] if enough time has passed since last execution. | |
* | |
* @param block Code to execute at the controlled rate | |
* @return true if block was executed, false if skipped due to rate limiting | |
*/ | |
fun sample(block: TimeSampler.() -> Unit): Boolean { | |
val currentMark = timeSource.markNow() | |
val lastMark = lastExecutionMark | |
// First execution always runs | |
if (lastMark == null) { | |
lastExecutionMark = currentMark | |
block() | |
return true | |
} | |
// Check if enough time has passed | |
val elapsed = lastMark.elapsedNow() | |
if (elapsed >= minInterval) { | |
lastExecutionMark = currentMark | |
block() | |
return true | |
} | |
return false | |
} | |
/** resets the sampler, allowing immediate execution on next [sample] call. */ | |
fun reset() { | |
lastExecutionMark = null | |
} | |
} | |
/** | |
* run a block every [runEvery] calls. | |
* With [runEvery]=60: | |
* - Default [count]=0: Executes on calls #1, #61, #121... | |
* - Custom [count]=5: Executes on calls #56, #116, #176... | |
* | |
* @param count provide a starting count to the sampler (if you want to shift the first execution) | |
*/ | |
class FrequencySampler(val runEvery: Int, private var count: Int = 0) { | |
fun sample(block: FrequencySampler.() -> Unit) { | |
if ((count % runEvery) == 0) block() | |
count++ | |
} | |
} | |
class FrequencySamplerWithTime( | |
runEvery: Int = 1500, | |
val timeSource: TimeSource = TimeSource.Monotonic, | |
) { | |
private val sampler = FrequencySampler(runEvery) | |
private var startMark: TimeMark? = null | |
private var lastMark: TimeMark? = null | |
// [timePerCall] - approximate time each unsampled call takes | |
fun sample(block: FrequencySamplerWithTime.(timePerCall: Duration) -> Unit = {}) { | |
// Init on first call. | |
if (lastMark == null) { | |
val now = timeSource.markNow() | |
startMark = now | |
lastMark = now | |
} | |
sampler.sample { | |
val currentMark = timeSource.markNow() | |
val diff = lastMark?.elapsedNow() ?: Duration.ZERO | |
block(diff / runEvery) | |
lastMark = currentMark | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment