Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions android-agent/api/android-agent.api
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public final class io/opentelemetry/android/agent/connectivity/Compression : jav
}

public final class io/opentelemetry/android/agent/dsl/DiskBufferingConfigurationSpec : io/opentelemetry/android/agent/dsl/instrumentation/CanBeEnabledAndDisabled {
public final fun autoDetectExportSchedule (Z)V
public fun enabled (Z)V
public final fun exportScheduleDelay (J)V
}

public final class io/opentelemetry/android/agent/dsl/EndpointConfiguration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ import io.opentelemetry.android.agent.dsl.instrumentation.CanBeEnabledAndDisable
class DiskBufferingConfigurationSpec internal constructor() : CanBeEnabledAndDisabled {
internal var enabled: Boolean = true

internal var exportScheduleDelay: Long? = null
internal var autoDetectExportSchedule: Boolean = false

override fun enabled(enabled: Boolean) {
this.enabled = enabled
}

fun exportScheduleDelay(limit: Long) {
this.exportScheduleDelay = limit
}

fun autoDetectExportSchedule(enabled: Boolean) {
this.autoDetectExportSchedule = enabled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package io.opentelemetry.android.agent.dsl
import io.opentelemetry.android.Incubating
import io.opentelemetry.android.agent.dsl.instrumentation.InstrumentationConfiguration
import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.android.features.diskbuffering.DEFAULT_EXPORT_SCHEDULE_DELAY_MS
import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfig
import io.opentelemetry.api.common.Attributes

Expand Down Expand Up @@ -61,6 +62,13 @@ class OpenTelemetryConfiguration internal constructor(
*/
fun diskBuffering(action: DiskBufferingConfigurationSpec.() -> Unit) {
diskBufferingConfig.action()
rumConfig.setDiskBufferingConfig(DiskBufferingConfig.create(enabled = diskBufferingConfig.enabled))
val delay = diskBufferingConfig.exportScheduleDelay ?: DEFAULT_EXPORT_SCHEDULE_DELAY_MS
rumConfig.setDiskBufferingConfig(
DiskBufferingConfig.create(
enabled = diskBufferingConfig.enabled,
exportScheduleDelayMillis = delay,
autoDetectExportSchedule = diskBufferingConfig.autoDetectExportSchedule,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,40 @@ class DiskBufferingConfigTest {
assertTrue(otelConfig.diskBufferingConfig.enabled)
assertTrue(otelConfig.rumConfig.getDiskBufferingConfig().enabled)
}

@Test
fun testOverrideExportDelayAndAutoDetect() {
val otelConfig = OpenTelemetryConfiguration()
otelConfig.diskBuffering {
enabled(true)
exportScheduleDelay(100)
autoDetectExportSchedule(true)
}
assertTrue(otelConfig.diskBufferingConfig.enabled)
assertTrue(otelConfig.rumConfig.getDiskBufferingConfig().enabled)
assertTrue(otelConfig.diskBufferingConfig.autoDetectExportSchedule)
assertTrue(otelConfig.rumConfig.getDiskBufferingConfig().autoDetectExportSchedule)

// DSL helper stores the raw value
assert(otelConfig.diskBufferingConfig.exportScheduleDelay == 100L)
// Config object enforces minimum 1000ms
assert(otelConfig.rumConfig.getDiskBufferingConfig().exportScheduleDelayMillis == 1000L)
}

@Test
fun testOverrideExportDelayAndAutoDetectValid() {
val otelConfig = OpenTelemetryConfiguration()
otelConfig.diskBuffering {
enabled(true)
exportScheduleDelay(5000)
autoDetectExportSchedule(true)
}

assertTrue(otelConfig.diskBufferingConfig.autoDetectExportSchedule)
assertTrue(otelConfig.rumConfig.getDiskBufferingConfig().autoDetectExportSchedule)

// Check delay matches what was set since it's > 1000ms
val storedConfig = otelConfig.rumConfig.getDiskBufferingConfig()
assert(storedConfig.exportScheduleDelayMillis == 5000L)
}
}
26 changes: 17 additions & 9 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,21 @@ 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 fun <init> (ZIJJJIZLjava/io/File;JZ)V
public synthetic fun <init> (ZIJJJIZLjava/io/File;JZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Z
public final fun component10 ()Z
public final fun component2 ()I
public final fun component3 ()J
public final fun component4 ()J
public final fun component5 ()J
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;JZ)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static synthetic fun copy$default (Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;ZIJJJIZLjava/io/File;JZILjava/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 +141,13 @@ 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 static final fun create (ZIJJJIZLjava/io/File;JZ)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getAutoDetectExportSchedule ()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 +168,13 @@ 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 final fun create (ZIJJJIZLjava/io/File;JZ)Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig;
public static synthetic fun create$default (Lio/opentelemetry/android/features/diskbuffering/DiskBufferingConfig$Companion;ZIJJJIZLjava/io/File;JZILjava/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_MS 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 +207,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 @@ -16,10 +16,12 @@ import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.android.export.BufferDelegatingLogExporter
import io.opentelemetry.android.export.BufferDelegatingMetricExporter
import io.opentelemetry.android.export.BufferDelegatingSpanExporter
import io.opentelemetry.android.features.diskbuffering.DEFAULT_EXPORT_SCHEDULE_DELAY_MS
import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter
import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter.Companion.set
import io.opentelemetry.android.features.diskbuffering.scheduler.DefaultExportScheduleHandler
import io.opentelemetry.android.features.diskbuffering.scheduler.DefaultExportScheduler
import io.opentelemetry.android.features.diskbuffering.scheduler.ExportScheduleAutoDetector
import io.opentelemetry.android.features.diskbuffering.scheduler.ExportScheduleHandler
import io.opentelemetry.android.instrumentation.AndroidInstrumentation
import io.opentelemetry.android.internal.features.networkattrs.NetworkAttributesLogRecordAppender
Expand Down Expand Up @@ -418,9 +420,26 @@ 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()

// Determine actual export delay: either auto-detected or user-configured
val exportDelay =
if (diskBufferingConfig.autoDetectExportSchedule) {
ExportScheduleAutoDetector.detectOptimalExportDelay(
context,
if (diskBufferingConfig.exportScheduleDelayMillis == DEFAULT_EXPORT_SCHEDULE_DELAY_MS) {
null // Auto-detect since using default
} else {
diskBufferingConfig.exportScheduleDelayMillis // Use user override
},
)
} else {
diskBufferingConfig.exportScheduleDelayMillis
}

val handler =
exportScheduleHandler ?: DefaultExportScheduleHandler(
DefaultExportScheduler(services::periodicWork),
DefaultExportScheduler(services::periodicWork, exportDelay),
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_MS: Long = 10000L

data class DiskBufferingConfig
@JvmOverloads
Expand All @@ -31,6 +32,65 @@ 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 10 seconds (10000 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.
*
* When [autoDetectExportSchedule] is true, this value is used as an override:
* - If explicitly set to a non-default value, it overrides auto-detection
* - If left at the default value, auto-detection will be used
*
* Trade-offs to consider:
* - Lower values (e.g., 10 seconds): More frequent exports mean fresher data in RUM sessions,
* but higher resource consumption (CPU, disk I/O, network activity) and potentially higher
* backend load from more frequent requests.
* - Higher values (e.g., 60 seconds or more): Reduced resource consumption and backend load,
* but longer delay before data becomes available in RUM sessions.
*
* Configuration recommendations:
* - 10000 ms (10 seconds): Default value, provides balance between data freshness and
* resource consumption for typical applications.
* - 5000 ms (5 seconds) or lower: For applications with critical real-time monitoring needs,
* where fresher data is worth the additional resource cost.
* - 30000 ms (30 seconds) or higher: For applications with high telemetry volume where reducing
* backend load and device resource consumption is prioritized over data freshness.
*
* Performance considerations:
* - Each export cycle may involve reading, serializing, and transmitting buffered signals.
* Higher export frequency means more frequent resource usage.
* - Lower export frequency means more data accumulates per export cycle, which could impact
* memory usage and delay in data availability.
*
* Minimum supported value: 1000 ms (1 second). Values less than this will be automatically
* increased to 1000 ms with a warning.
*
* Best practice: Test your configuration with your specific telemetry volume and workload
* to find the optimal balance for your use case.
*/
val exportScheduleDelayMillis: Long = DEFAULT_EXPORT_SCHEDULE_DELAY_MS,
/**
* Enables automatic detection of optimal export schedule based on device conditions.
*
* When enabled, the SDK will analyze device state and adjust export frequency accordingly:
* - On low battery or battery saver: Increase interval (less frequent exports)
* - Under memory pressure: Increase interval (reduce memory consumption)
* - Normal conditions: Use default or user-configured interval
*
* Auto-detection respects user configuration:
* - If [exportScheduleDelayMillis] is explicitly set to a non-default value,
* it will be used (auto-detection is overridden)
* - If left at default value, auto-detection suggestions are applied
*
* Default: false (preserves current behavior - no auto-detection)
*
* This feature is useful for:
* - Applications that need to adapt to device conditions
* - High-volume telemetry scenarios where battery life is critical
* - Scenarios where users want the best of both worlds: sensible defaults plus explicit control
*/
Copy link
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 autoDetectExportSchedule: Boolean = false,
) {
companion object {
/**
Expand All @@ -49,12 +109,34 @@ data class DiskBufferingConfig
maxCacheFileSize: Int = MAX_CACHE_FILE_SIZE,
debugEnabled: Boolean = false,
signalsBufferDir: File? = null,
exportScheduleDelayMillis: Long = DEFAULT_EXPORT_SCHEDULE_DELAY_MS,
autoDetectExportSchedule: Boolean = false,
): 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")
try {
Log.w(OTEL_RUM_LOG_TAG, "minFileAgeForReadMillis must be greater than maxFileAgeForWriteMillis")
Log.w(
OTEL_RUM_LOG_TAG,
"overriding minFileAgeForReadMillis from $minFileAgeForReadMillis to $minRead",
)
} catch (e: RuntimeException) {
// Keep going, this is just a warning, and we might be running in a unit test
}
Comment on lines +118 to +126
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

Wrapping logging statements in a try-catch for RuntimeException is unusual and may hide legitimate issues. If this is specifically for unit test environments where logging might fail, consider a more explicit check or documentation explaining why this pattern is necessary.

Copilot uses AI. Check for mistakes.
}
var validatedExportDelay = exportScheduleDelayMillis
if (exportScheduleDelayMillis < 1000L) {
validatedExportDelay = 1000L
try {
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",
)
} catch (e: RuntimeException) {
// Keep going, this is just a warning, and we might be running in a unit test
}
}
return DiskBufferingConfig(
enabled = enabled,
Expand All @@ -65,6 +147,8 @@ data class DiskBufferingConfig
maxCacheFileSize = maxCacheFileSize,
debugEnabled = debugEnabled,
signalsBufferDir = signalsBufferDir,
exportScheduleDelayMillis = validatedExportDelay,
autoDetectExportSchedule = autoDetectExportSchedule,
)
}
}
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.SECONDS.toMillis(10),
Copy link
Contributor

Choose a reason for hiding this comment

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

@breedx-splk do we use @JVMOverloads constructor to have nicer api and good backward compatibility support in case of default param? If yes we should add the same here

Copy link
Contributor

Choose a reason for hiding this comment

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

It's only a "nice-to-have" if we expect java users to be calling it.

) : 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
}
Loading