Skip to content

Commit 8d40374

Browse files
authored
Merge pull request #1536 from DataDog/nogorodnikov/prepare-1.19.3
Prepare release 1.19.3
2 parents adfa6e8 + e80d7c7 commit 8d40374

File tree

6 files changed

+137
-9
lines changed

6 files changed

+137
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.19.3 / 2023-07-11
2+
3+
* [IMPROVEMENT] RUM: Introduce known file cache and cleanup throttling in `BatchFileOrchestrator` in order to reduce the number of syscalls. See [#1506](https://github.com/DataDog/dd-sdk-android/pull/1506)
4+
15
# 1.19.2 / 2023-06-05
26

37
* [REVERT] RUM: Force new session at SDK initialization. See [#1399](https://github.com/DataDog/dd-sdk-android/pull/1399)

buildSrc/src/main/kotlin/com/datadog/gradle/config/AndroidConfig.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ object AndroidConfig {
1919
const val MIN_SDK_FOR_WEAR = 23
2020
const val BUILD_TOOLS_VERSION = "33.0.0"
2121

22-
val VERSION = Version(1, 19, 2, Version.Type.Release)
22+
val VERSION = Version(1, 19, 3, Version.Type.Release)
2323
}
2424

2525
@Suppress("UnstableApiUsage")

dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FilePersistenceConfig.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ internal data class FilePersistenceConfig(
1515
val maxItemSize: Long = MAX_ITEM_SIZE,
1616
val maxItemsPerBatch: Int = MAX_ITEMS_PER_BATCH,
1717
val oldFileThreshold: Long = OLD_FILE_THRESHOLD,
18-
val maxDiskSpace: Long = MAX_DISK_SPACE
18+
val maxDiskSpace: Long = MAX_DISK_SPACE,
19+
val cleanupFrequencyThreshold: Long = CLEANUP_FREQUENCY_THRESHOLD_MS
1920
) {
2021
companion object {
2122
internal const val MAX_BATCH_SIZE: Long = 4L * 1024 * 1024 // 4 MB
@@ -24,5 +25,6 @@ internal data class FilePersistenceConfig(
2425
internal const val OLD_FILE_THRESHOLD: Long = 18L * 60L * 60L * 1000L // 18 hours
2526
internal const val MAX_DISK_SPACE: Long = 128 * MAX_BATCH_SIZE // 512 MB
2627
internal const val MAX_DELAY_BETWEEN_MESSAGES_MS = 5000L
28+
internal const val CLEANUP_FREQUENCY_THRESHOLD_MS = 1000L // 1s
2729
}
2830
}

dd-sdk-android/src/main/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator.kt

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package com.datadog.android.core.internal.persistence.file.batch
88

99
import androidx.annotation.WorkerThread
10+
import androidx.collection.LruCache
1011
import com.datadog.android.core.internal.persistence.file.FileOrchestrator
1112
import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig
1213
import com.datadog.android.core.internal.persistence.file.canWriteSafe
@@ -22,6 +23,8 @@ import java.io.FileFilter
2223
import java.util.Locale
2324
import kotlin.math.roundToLong
2425

26+
// TODO RUMM-3373 Improve this class: need to make it thread-safe and optimize work with file
27+
// system in order to reduce the number of syscalls (which are expensive) for files already seen
2528
internal class BatchFileOrchestrator(
2629
private val rootDir: File,
2730
private val config: FilePersistenceConfig,
@@ -39,6 +42,10 @@ internal class BatchFileOrchestrator(
3942
private var previousFile: File? = null
4043
private var previousFileItemCount: Int = 0
4144

45+
@Suppress("UnsafeThirdPartyFunctionCall") // argument is not negative
46+
private val knownBatchFiles = LruCache<File, Unit>(KNOWN_FILES_MAX_CACHE_SIZE)
47+
private var lastCleanupTimestamp: Long = 0L
48+
4249
// region FileOrchestrator
4350

4451
@WorkerThread
@@ -47,8 +54,11 @@ internal class BatchFileOrchestrator(
4754
return null
4855
}
4956

50-
deleteObsoleteFiles()
51-
freeSpaceIfNeeded()
57+
if (canDoCleanup()) {
58+
deleteObsoleteFiles()
59+
freeSpaceIfNeeded()
60+
lastCleanupTimestamp = System.currentTimeMillis()
61+
}
5262

5363
return if (!forceNewFile) {
5464
getReusableWritableFile() ?: createNewFile()
@@ -64,6 +74,7 @@ internal class BatchFileOrchestrator(
6474
}
6575

6676
deleteObsoleteFiles()
77+
lastCleanupTimestamp = System.currentTimeMillis()
6778

6879
val files = listSortedBatchFiles()
6980

@@ -183,6 +194,8 @@ internal class BatchFileOrchestrator(
183194
val newFile = File(rootDir, newFileName)
184195
previousFile = newFile
185196
previousFileItemCount = 1
197+
@Suppress("UnsafeThirdPartyFunctionCall") // value is not null
198+
knownBatchFiles.put(newFile, Unit)
186199
return newFile
187200
}
188201

@@ -228,6 +241,8 @@ internal class BatchFileOrchestrator(
228241
.filter { (it.name.toLongOrNull() ?: 0) < threshold }
229242
.forEach {
230243
it.deleteSafe()
244+
@Suppress("UnsafeThirdPartyFunctionCall") // value is not null
245+
knownBatchFiles.remove(it)
231246
if (it.metadata.existsSafe()) {
232247
it.metadata.deleteSafe()
233248
}
@@ -261,6 +276,8 @@ internal class BatchFileOrchestrator(
261276
if (!file.existsSafe()) return 0
262277

263278
val size = file.lengthSafe()
279+
@Suppress("UnsafeThirdPartyFunctionCall") // value is not null
280+
knownBatchFiles.remove(file)
264281
return if (file.deleteSafe()) {
265282
size
266283
} else {
@@ -275,15 +292,33 @@ internal class BatchFileOrchestrator(
275292
private val File.metadata: File
276293
get() = File("${this.path}_metadata")
277294

295+
private fun canDoCleanup(): Boolean {
296+
return System.currentTimeMillis() - lastCleanupTimestamp > config.cleanupFrequencyThreshold
297+
}
298+
278299
// endregion
279300

280301
// region FileFilter
281302

282-
internal class BatchFileFilter : FileFilter {
303+
internal inner class BatchFileFilter : FileFilter {
304+
@Suppress("ReturnCount")
283305
override fun accept(file: File?): Boolean {
284-
return file != null &&
285-
file.isFileSafe() &&
306+
if (file == null) return false
307+
308+
@Suppress("UnsafeThirdPartyFunctionCall") // value is not null
309+
if (knownBatchFiles.get(file) != null) {
310+
return true
311+
}
312+
313+
return if (file.isFileSafe() &&
286314
file.name.matches(batchFileNameRegex)
315+
) {
316+
@Suppress("UnsafeThirdPartyFunctionCall") // both values are not null
317+
knownBatchFiles.put(file, Unit)
318+
true
319+
} else {
320+
false
321+
}
287322
}
288323
}
289324

@@ -294,6 +329,10 @@ internal class BatchFileOrchestrator(
294329
const val DECREASE_PERCENT = 0.95
295330
const val INCREASE_PERCENT = 1.05
296331

332+
// File class contains only few simple fields, so retained size usually is way below even 1Kb.
333+
// Holding 400 items at max will be below 400 Kb of retained size.
334+
private const val KNOWN_FILES_MAX_CACHE_SIZE = 400
335+
297336
private val batchFileNameRegex = Regex("\\d+")
298337
internal const val ERROR_ROOT_NOT_WRITABLE = "The provided root dir is not writable: %s"
299338
internal const val ERROR_ROOT_NOT_DIR = "The provided root file is not a directory: %s"

dd-sdk-android/src/test/kotlin/com/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestratorTest.kt

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import java.util.concurrent.TimeUnit
4747
@MockitoSettings(strictness = Strictness.LENIENT)
4848
internal class BatchFileOrchestratorTest {
4949

50-
lateinit var testedOrchestrator: FileOrchestrator
50+
private lateinit var testedOrchestrator: FileOrchestrator
5151

5252
@TempDir
5353
lateinit var tempDir: File
@@ -202,6 +202,81 @@ internal class BatchFileOrchestratorTest {
202202
assertThat(youngFile).exists()
203203
}
204204

205+
@ParameterizedTest
206+
@ValueSource(booleans = [true, false])
207+
fun `𝕄 respect time threshold to delete obsolete files 𝕎 getWritableFile() { below threshold }`(
208+
forceNewFile: Boolean,
209+
@LongForgery(min = OLD_FILE_THRESHOLD, max = Int.MAX_VALUE.toLong()) oldFileAge: Long
210+
) {
211+
// Given
212+
assumeTrue(fakeRootDir.listFiles().isNullOrEmpty())
213+
val oldTimestamp = System.currentTimeMillis() - oldFileAge
214+
val oldFile = File(fakeRootDir, oldTimestamp.toString())
215+
oldFile.createNewFile()
216+
val oldFileMeta = File("${oldFile.path}_metadata")
217+
oldFileMeta.createNewFile()
218+
val youngTimestamp = System.currentTimeMillis() - RECENT_DELAY_MS - 1
219+
val youngFile = File(fakeRootDir, youngTimestamp.toString())
220+
youngFile.createNewFile()
221+
222+
// When
223+
val start = System.currentTimeMillis()
224+
val result = testedOrchestrator.getWritableFile(forceNewFile)
225+
val end = System.currentTimeMillis()
226+
// let's add very old file after the previous cleanup call. If threshold is respected,
227+
// cleanup shouldn't be performed during the next getWritableFile call
228+
val evenOlderFile = File(fakeRootDir, (oldTimestamp - 1).toString())
229+
evenOlderFile.createNewFile()
230+
testedOrchestrator.getWritableFile(forceNewFile)
231+
232+
// Then
233+
checkNotNull(result)
234+
assertThat(result)
235+
.doesNotExist()
236+
.hasParent(fakeRootDir)
237+
assertThat(result.name.toLong())
238+
.isBetween(start, end)
239+
assertThat(oldFile).doesNotExist()
240+
assertThat(oldFileMeta).doesNotExist()
241+
assertThat(youngFile).exists()
242+
assertThat(evenOlderFile).exists()
243+
}
244+
245+
@ParameterizedTest
246+
@ValueSource(booleans = [true, false])
247+
fun `𝕄 respect time threshold to delete obsolete files 𝕎 getWritableFile() { above threshold }`(
248+
forceNewFile: Boolean,
249+
@LongForgery(min = OLD_FILE_THRESHOLD, max = Int.MAX_VALUE.toLong()) oldFileAge: Long
250+
) {
251+
// Given
252+
assumeTrue(fakeRootDir.listFiles().isNullOrEmpty())
253+
val oldTimestamp = System.currentTimeMillis() - oldFileAge
254+
val oldFile = File(fakeRootDir, oldTimestamp.toString())
255+
oldFile.createNewFile()
256+
val oldFileMeta = File("${oldFile.path}_metadata")
257+
oldFileMeta.createNewFile()
258+
259+
// When
260+
val start = System.currentTimeMillis()
261+
val result = testedOrchestrator.getWritableFile(forceNewFile)
262+
val end = System.currentTimeMillis()
263+
Thread.sleep(CLEANUP_FREQUENCY_THRESHOLD_MS + 1)
264+
val evenOlderFile = File(fakeRootDir, (oldTimestamp - 1).toString())
265+
evenOlderFile.createNewFile()
266+
testedOrchestrator.getWritableFile(forceNewFile)
267+
268+
// Then
269+
checkNotNull(result)
270+
assertThat(result)
271+
.doesNotExist()
272+
.hasParent(fakeRootDir)
273+
assertThat(result.name.toLong())
274+
.isBetween(start, end)
275+
assertThat(oldFile).doesNotExist()
276+
assertThat(oldFileMeta).doesNotExist()
277+
assertThat(evenOlderFile).doesNotExist()
278+
}
279+
205280
@ParameterizedTest
206281
@ValueSource(booleans = [true, false])
207282
fun `𝕄 return new File 𝕎 getWritableFile() {no available file}`(forceNewFile: Boolean) {
@@ -413,6 +488,7 @@ internal class BatchFileOrchestratorTest {
413488
}
414489

415490
// When
491+
Thread.sleep(CLEANUP_FREQUENCY_THRESHOLD_MS + 1)
416492
val start = System.currentTimeMillis()
417493
val result = testedOrchestrator.getWritableFile(forceNewFile)
418494
val end = System.currentTimeMillis()
@@ -1021,13 +1097,16 @@ internal class BatchFileOrchestratorTest {
10211097
private const val OLD_FILE_THRESHOLD: Long = RECENT_DELAY_MS * 4
10221098
private const val MAX_DISK_SPACE = MAX_BATCH_SIZE * 4
10231099

1100+
private const val CLEANUP_FREQUENCY_THRESHOLD_MS = 50L
1101+
10241102
private val TEST_PERSISTENCE_CONFIG = FilePersistenceConfig(
10251103
RECENT_DELAY_MS,
10261104
MAX_BATCH_SIZE.toLong(),
10271105
MAX_ITEM_SIZE.toLong(),
10281106
MAX_ITEM_PER_BATCH,
10291107
OLD_FILE_THRESHOLD,
1030-
MAX_DISK_SPACE.toLong()
1108+
MAX_DISK_SPACE.toLong(),
1109+
CLEANUP_FREQUENCY_THRESHOLD_MS
10311110
)
10321111
}
10331112
}

detekt_custom.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ datadog:
104104
- "androidx.work.WorkManager.enqueueUniqueWork(kotlin.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest):java.util.concurrent.RejectedExecutionException,java.lang.NullPointerException"
105105
- "android.content.res.Resources.getResourceName(kotlin.Int):android.content.res.Resources.NotFoundException"
106106
- "android.content.res.Resources.openRawResource(kotlin.Int):android.content.res.Resources.NotFoundException"
107+
- "androidx.collection.LruCache.constructor(kotlin.Int):java.lang.IllegalArgumentException"
108+
- "androidx.collection.LruCache.get(java.io.File):java.lang.NullPointerException"
109+
- "androidx.collection.LruCache.put(java.io.File, kotlin.Unit):java.lang.NullPointerException"
110+
- "androidx.collection.LruCache.remove(java.io.File):java.lang.NullPointerException"
107111
# endregion
108112
# region Java File
109113
- "java.io.ByteArrayOutputStream.write(kotlin.ByteArray, kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException"

0 commit comments

Comments
 (0)