Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
7bfcc18
feat: optimize disk buffering export frequency with configurable delay
namanONcode Dec 3, 2025
b853d1a
feat: add configurable export frequency for disk buffering to optimiz…
namanONcode Dec 3, 2025
7b11fe0
removed ai generated documentation
namanONcode Dec 3, 2025
b58a737
feat: reduce default export schedule delay to 10 seconds for improved…
namanONcode Dec 3, 2025
00e810b
feat: implement auto-detection for optimal export frequency based on …
namanONcode Dec 3, 2025
c7fa8b4
refactor: replace for loop with repeat function for improved readabil…
namanONcode Dec 3, 2025
ce13cf3
refactor: improve code readability by standardizing formatting in Exp…
namanONcode Dec 3, 2025
c32a8ca
test: enhance unit tests for ExportScheduleAutoDetector with comprehe…
namanONcode Dec 3, 2025
c991106
refactor: remove unused imports and improve code clarity in ExportSch…
namanONcode Dec 3, 2025
693b7f0
refactor: standardize formatting in ExportScheduleAutoDetectorTest fo…
namanONcode Dec 3, 2025
efd0b03
refactor: rename export schedule delay constants for consistency and …
namanONcode Dec 9, 2025
46499d3
refactor: remove trailing whitespace in ExportScheduleAutoDetectorTes…
namanONcode Dec 9, 2025
6197112
feat: enable configuration of disk buffering export schedule delay an…
namanONcode Dec 16, 2025
5a7756c
feat: Add auto-detect and delay configuration for disk buffering expo…
namanONcode Dec 16, 2025
cb82493
feat: externalize memory info retrieval for testability and enhance e…
namanONcode Dec 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ public final class io/opentelemetry/android/features/diskbuffering/DiskBuffering
public fun <init> (ZIJJJI)V
public fun <init> (ZIJJJIZ)V
public fun <init> (ZIJJJIZLjava/io/File;)V
public synthetic fun <init> (ZIJJJIZLjava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZIJJJIZLjava/io/File;J)V
public synthetic fun <init> (ZIJJJIZLjava/io/File;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Z
public final fun component2 ()I
public final fun component3 ()J
Expand All @@ -126,8 +127,9 @@ public final class io/opentelemetry/android/features/diskbuffering/DiskBuffering
public final fun component6 ()I
public final fun component7 ()Z
public final fun component8 ()Ljava/io/File;
public final fun copy (ZIJJJIZLjava/io/File;)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static synthetic fun copy$default (Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;ZIJJJIZLjava/io/File;ILjava/lang/Object;)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public final fun component9 ()J
public final fun copy (ZIJJJIZLjava/io/File;J)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static synthetic fun copy$default (Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;ZIJJJIZLjava/io/File;JILjava/lang/Object;)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static final fun create ()Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static final fun create (Z)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static final fun create (ZI)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
Expand All @@ -137,9 +139,11 @@ public final class io/opentelemetry/android/features/diskbuffering/DiskBuffering
public static final fun create (ZIJJJI)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static final fun create (ZIJJJIZ)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static final fun create (ZIJJJIZLjava/io/File;)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static final fun create (ZIJJJIZLjava/io/File;J)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getDebugEnabled ()Z
public final fun getEnabled ()Z
public final fun getExportScheduleDelayMillis ()J
public final fun getMaxCacheFileSize ()I
public final fun getMaxCacheSize ()I
public final fun getMaxFileAgeForReadMillis ()J
Expand All @@ -160,10 +164,12 @@ public final class io/opentelemetry/android/features/diskbuffering/DiskBuffering
public final fun create (ZIJJJI)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public final fun create (ZIJJJIZ)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public final fun create (ZIJJJIZLjava/io/File;)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static synthetic fun create$default (Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig$Companion;ZIJJJIZLjava/io/File;ILjava/lang/Object;)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public final fun create (ZIJJJIZLjava/io/File;J)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static synthetic fun create$default (Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig$Companion;ZIJJJIZLjava/io/File;JILjava/lang/Object;)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
}

public final class io/opentelemetry/android/features/diskbuffering/DiskBufferingConfigKt {
public static final field DEFAULT_EXPORT_SCHEDULE_DELAY_MILLIS J
public static final field DEFAULT_MAX_CACHE_SIZE I
public static final field DEFAULT_MAX_FILE_AGE_FOR_READ_MS J
public static final field DEFAULT_MAX_FILE_AGE_FOR_WRITE_MS J
Expand Down Expand Up @@ -196,17 +202,14 @@ public final class io/opentelemetry/android/features/diskbuffering/scheduler/Def
}

public final class io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler : io/opentelemetry/android/internal/services/periodicwork/PeriodicRunnable {
public static final field Companion Lio/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler$Companion;
public fun <init> (Lkotlin/jvm/functions/Function0;)V
public fun <init> (Lkotlin/jvm/functions/Function0;J)V
public synthetic fun <init> (Lkotlin/jvm/functions/Function0;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun minimumDelayUntilNextRunInMillis ()J
public fun onRun ()V
public fun shouldStopRunning ()Z
public final fun shutdown ()V
}

public final class io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler$Companion {
}

public abstract interface class io/opentelemetry/android/features/diskbuffering/scheduler/ExportScheduleHandler {
public fun disable ()V
public abstract fun enable ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,10 @@ class OpenTelemetryRumBuilder internal constructor(
) {
// TODO: Is it safe to get the work service yet here? If so, we can
// avoid all this lazy supplier stuff....
val diskBufferingConfig = config.getDiskBufferingConfig()
val handler =
exportScheduleHandler ?: DefaultExportScheduleHandler(
DefaultExportScheduler(services::periodicWork),
DefaultExportScheduler(services::periodicWork, diskBufferingConfig.exportScheduleDelayMillis),
services::periodicWork,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const val MAX_CACHE_FILE_SIZE: Int = 1024 * 1024
const val DEFAULT_MAX_FILE_AGE_FOR_WRITE_MS = 30L
const val DEFAULT_MIN_FILE_AGE_FOR_READ_MS = 33L
const val DEFAULT_MAX_FILE_AGE_FOR_READ_MS = 18L
const val DEFAULT_EXPORT_SCHEDULE_DELAY_MILLIS: Long = 60000L // 1 minute

data class DiskBufferingConfig
@JvmOverloads
Expand All @@ -31,6 +32,35 @@ data class DiskBufferingConfig
* `null`, a default directory inside the application's cache directory will be used.
*/
val signalsBufferDir: File? = null,
/**
* The delay in milliseconds between consecutive export attempts. Defaults to 1 minute (60000 ms).
*
* This value controls how frequently the SDK attempts to export buffered signals from disk.
* The configured value represents the minimum delay between export attempts. Due to the
* periodic work scheduling mechanism, the actual export frequency may be limited by the
* loop interval of the periodic work executor (default: 60 seconds).
*
* Recommended values:
* - 60000 ms (1 minute) or higher: Standard configuration, provides good balance between
* data freshness and resource consumption. This matches the default periodic work loop
* interval and ensures exports happen at the configured frequency.
* - 300000 ms (5 minutes) or higher: For high-volume scenarios where reducing backend load
* and battery consumption is critical. Suitable for applications where near-real-time
* data is not essential.
* - Values less than 60000 ms: Not recommended. While the SDK supports values down to
* 1000 ms (1 second), the periodic work executor's 60-second loop interval may prevent
* the configured frequency from being achieved. If you configure a value less than
* 60 seconds, the actual export frequency will still be approximately 60 seconds.
*
* Important: For each 8-hour workday, configure thoughtfully to balance between:
* - Data freshness (prefer lower values)
* - Device battery consumption (prefer higher values)
* - Backend load (prefer higher values)
*
* Example impact: A 10-second export interval means ~2880 export attempts per 8-hour day.
* A 60-second interval reduces this to ~480 attempts. A 5-minute interval reduces it to ~96 attempts.
*/
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.

I think this comment is too long.

val exportScheduleDelayMillis: Long = DEFAULT_EXPORT_SCHEDULE_DELAY_MILLIS,
) {
companion object {
/**
Expand All @@ -49,13 +79,36 @@ data class DiskBufferingConfig
maxCacheFileSize: Int = MAX_CACHE_FILE_SIZE,
debugEnabled: Boolean = false,
signalsBufferDir: File? = null,
exportScheduleDelayMillis: Long = DEFAULT_EXPORT_SCHEDULE_DELAY_MILLIS,
): DiskBufferingConfig {
var minRead = minFileAgeForReadMillis
if (minFileAgeForReadMillis <= maxFileAgeForWriteMillis) {
minRead = maxFileAgeForWriteMillis + 5
Log.w(OTEL_RUM_LOG_TAG, "minFileAgeForReadMillis must be greater than maxFileAgeForWriteMillis")
Log.w(OTEL_RUM_LOG_TAG, "overriding minFileAgeForReadMillis from $minFileAgeForReadMillis to $minRead")
}
var validatedExportDelay = exportScheduleDelayMillis
if (exportScheduleDelayMillis < 1000L) {
validatedExportDelay = 1000L
Log.w(OTEL_RUM_LOG_TAG, "exportScheduleDelayMillis must be at least 1000 ms (1 second)")
Log.w(OTEL_RUM_LOG_TAG, "overriding exportScheduleDelayMillis from $exportScheduleDelayMillis to $validatedExportDelay")
} else if (exportScheduleDelayMillis < 60000L) {
// Warn users about the periodic work loop interval limitation
Log.w(
OTEL_RUM_LOG_TAG,
"exportScheduleDelayMillis is set to $exportScheduleDelayMillis ms, which is less " +
"than the periodic work loop interval (60000 ms)",
)
Log.w(
OTEL_RUM_LOG_TAG,
"The actual export frequency may be limited to approximately 60 seconds " +
"regardless of the configured value",
)
Log.w(
OTEL_RUM_LOG_TAG,
"Consider using 60000 ms (1 minute) or higher for more predictable export behavior",
)
}
return DiskBufferingConfig(
enabled = enabled,
maxCacheSize = maxCacheSize,
Expand All @@ -65,6 +118,7 @@ data class DiskBufferingConfig
maxCacheFileSize = maxCacheFileSize,
debugEnabled = debugEnabled,
signalsBufferDir = signalsBufferDir,
exportScheduleDelayMillis = validatedExportDelay,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ import java.util.concurrent.TimeUnit

class DefaultExportScheduler(
periodicWorkProvider: () -> PeriodicWork,
private val exportScheduleDelayMillis: Long = TimeUnit.MINUTES.toMillis(1),
) : PeriodicRunnable(periodicWorkProvider) {
@Volatile
private var isShutDown: Boolean = false

companion object {
private val DELAY_BEFORE_NEXT_EXPORT_IN_MILLIS = TimeUnit.SECONDS.toMillis(10)
}

override fun onRun() {
val exporter = SignalFromDiskExporter.get() ?: return

Expand All @@ -41,5 +38,5 @@ class DefaultExportScheduler(

override fun shouldStopRunning(): Boolean = isShutDown || (SignalFromDiskExporter.get() == null)

override fun minimumDelayUntilNextRunInMillis(): Long = DELAY_BEFORE_NEXT_EXPORT_IN_MILLIS
override fun minimumDelayUntilNextRunInMillis(): Long = exportScheduleDelayMillis
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class DefaultExportScheduleHandlerTest {
periodicWork = createPeriodicWorkServiceMock()
handler =
DefaultExportScheduleHandler(
DefaultExportScheduler { periodicWork },
DefaultExportScheduler(periodicWorkProvider = { periodicWork }),
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.

this can be reverted

) { periodicWork }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,14 @@ class DefaultExportSchedulerTest {
@Test
fun `Verify minimum delay`() {
assertThat(scheduler.minimumDelayUntilNextRunInMillis()).isEqualTo(
TimeUnit.SECONDS.toMillis(
10,
),
TimeUnit.MINUTES.toMillis(1),
)
}

@Test
fun `Verify custom delay can be set`() {
val customDelay = TimeUnit.SECONDS.toMillis(30)
val customScheduler = DefaultExportScheduler(mockk(), customDelay)
assertThat(customScheduler.minimumDelayUntilNextRunInMillis()).isEqualTo(customDelay)
}
}
6 changes: 6 additions & 0 deletions services/api/services.api
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,15 @@ public abstract class io/opentelemetry/android/internal/services/periodicwork/Pe
}

public abstract interface class io/opentelemetry/android/internal/services/periodicwork/PeriodicWork : io/opentelemetry/android/internal/services/Service {
public static final field Companion Lio/opentelemetry/android/internal/services/periodicwork/PeriodicWork$Companion;
public static final field DEFAULT_LOOP_INTERVAL_MILLIS J
public abstract fun enqueue (Ljava/lang/Runnable;)V
}

public final class io/opentelemetry/android/internal/services/periodicwork/PeriodicWork$Companion {
public static final field DEFAULT_LOOP_INTERVAL_MILLIS J
}

public abstract interface class io/opentelemetry/android/internal/services/storage/CacheStorage : io/opentelemetry/android/internal/services/Service {
public abstract fun getCacheDir ()Ljava/io/File;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ import io.opentelemetry.android.internal.services.Service

interface PeriodicWork : Service {
fun enqueue(runnable: Runnable)

companion object {
/**
* Default loop interval in milliseconds. This determines how often the periodic work
* queue is checked for pending tasks. Note that the actual execution of scheduled work
* may be delayed beyond this interval depending on when tasks are enqueued and their
* minimum delay requirements.
*/
const val DEFAULT_LOOP_INTERVAL_MILLIS: Long = 60000L // 1 minute
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ import java.util.concurrent.atomic.AtomicBoolean
*
* <p>This class is internal and not for public use. Its APIs are unstable and can change at any
* time.
*
* <p>The loop interval determines how frequently this service checks its work queue for pending
* tasks. For optimal performance with exporters, the loop interval should typically match or be
* slightly shorter than the export frequency configured in the exporter (e.g.,
* exportScheduleDelayMillis). If the loop interval is significantly longer than the export
* frequency, the actual export timing may be delayed.
*/
internal class PeriodicWorkImpl : PeriodicWork {
private val delegator = WorkerDelegator()
internal class PeriodicWorkImpl(
private val loopIntervalMillis: Long = PeriodicWork.DEFAULT_LOOP_INTERVAL_MILLIS,
) : PeriodicWork {
private val delegator = WorkerDelegator(loopIntervalMillis)

init {
delegator.run()
Expand All @@ -35,12 +43,17 @@ internal class PeriodicWorkImpl : PeriodicWork {
delegator.close()
}

private class WorkerDelegator :
Runnable,
companion object {
// The minimum loop interval is 1 second to allow for flexible scheduling
internal const val MINIMUM_LOOP_INTERVAL_MILLIS: Long = 1000L // 1 second
}

private class WorkerDelegator(
private val loopIntervalMillis: Long,
) : Runnable,
Closeable {
companion object {
private const val SECONDS_TO_KILL_IDLE_THREADS = 30L
private const val SECONDS_FOR_NEXT_LOOP = 10L
private const val MAX_AMOUNT_OF_WORKER_THREADS = 1
private const val NUMBER_OF_PERMANENT_WORKER_THREADS = 0
}
Expand Down Expand Up @@ -86,7 +99,7 @@ internal class PeriodicWorkImpl : PeriodicWork {
}

private fun scheduleNextLookUp() {
handler.postDelayed(this, TimeUnit.SECONDS.toMillis(SECONDS_FOR_NEXT_LOOP))
handler.postDelayed(this, loopIntervalMillis)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class PeriodicWorkTest {
companion object {
private const val DELAY_BETWEEN_EXECUTIONS_IN_SECONDS = 10L
private const val DELAY_BETWEEN_EXECUTIONS_IN_SECONDS = 60L
}

private lateinit var service: PeriodicWork
Expand Down