Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REPLAY-1890: Implement pool of reusable bitmaps #1554

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ data class com.datadog.android.sessionreplay.internal.recorder.SystemInformation
class com.datadog.android.sessionreplay.internal.recorder.base64.Base64Serializer
interface com.datadog.android.sessionreplay.internal.recorder.base64.ImageCompression
fun getMimeType(): String?
fun compressBitmapToStream(android.graphics.Bitmap): java.io.ByteArrayOutputStream
fun compressBitmap(android.graphics.Bitmap): ByteArray
class com.datadog.android.sessionreplay.internal.recorder.base64.WebPImageCompression : ImageCompression
override fun getMimeType(): String?
override fun compressBitmapToStream(android.graphics.Bitmap): java.io.ByteArrayOutputStream
override fun compressBitmap(android.graphics.Bitmap): ByteArray
companion object
abstract class com.datadog.android.sessionreplay.internal.recorder.mapper.BaseWireframeMapper<T: android.view.View, S: com.datadog.android.sessionreplay.model.MobileSegment.Wireframe> : WireframeMapper<T, S>, com.datadog.android.sessionreplay.internal.AsyncImageProcessingCallback
constructor(com.datadog.android.sessionreplay.utils.StringUtils = StringUtils, com.datadog.android.sessionreplay.utils.ViewUtils = ViewUtils, com.datadog.android.sessionreplay.internal.recorder.base64.ImageCompression = WebPImageCompression(), com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator = UniqueIdentifierGenerator, com.datadog.android.sessionreplay.internal.recorder.base64.Base64Serializer = Base64Serializer.Builder().build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,17 @@ public final class com/datadog/android/sessionreplay/internal/recorder/SystemInf
}

public final class com/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer {
public synthetic fun <init> (Ljava/util/concurrent/ExecutorService;Lcom/datadog/android/sessionreplay/internal/utils/DrawableUtils;Lcom/datadog/android/sessionreplay/internal/utils/Base64Utils;Lcom/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression;Lcom/datadog/android/sessionreplay/internal/recorder/base64/Cache;Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/util/concurrent/ExecutorService;Lcom/datadog/android/sessionreplay/internal/utils/DrawableUtils;Lcom/datadog/android/sessionreplay/internal/utils/Base64Utils;Lcom/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression;Lcom/datadog/android/sessionreplay/internal/recorder/base64/Cache;Lcom/datadog/android/sessionreplay/internal/recorder/base64/Cache;Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public abstract interface class com/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression {
public abstract fun compressBitmapToStream (Landroid/graphics/Bitmap;)Ljava/io/ByteArrayOutputStream;
public abstract fun compressBitmap (Landroid/graphics/Bitmap;)[B
public abstract fun getMimeType ()Ljava/lang/String;
}

public final class com/datadog/android/sessionreplay/internal/recorder/base64/WebPImageCompression : com/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression {
public static final field Companion Lcom/datadog/android/sessionreplay/internal/recorder/base64/WebPImageCompression$Companion;
public fun compressBitmapToStream (Landroid/graphics/Bitmap;)Ljava/io/ByteArrayOutputStream;
public fun compressBitmap (Landroid/graphics/Bitmap;)[B
public fun getMimeType ()Ljava/lang/String;
}

Expand Down
5 changes: 5 additions & 0 deletions features/dd-sdk-android-session-replay/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ plugins {
id("com.github.ben-manes.versions")

// Tests
id("de.mobilej.unmock")
id("org.jetbrains.kotlinx.kover")

// Internal Generation
Expand Down Expand Up @@ -111,6 +112,10 @@ dependencies {
apply(from = "clone_session_replay_schema.gradle.kts")
apply(from = "generate_session_replay_models.gradle.kts")

unMock {
keep("android.util.LruCache")
}

kotlinConfig(jvmBytecodeTarget = JvmTarget.JVM_11)
junitConfig()
javadocConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@

package com.datadog.android.sessionreplay.internal.async

import android.graphics.drawable.Drawable
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.datadog.android.sessionreplay.internal.processor.RecordedDataProcessor
import com.datadog.android.sessionreplay.internal.processor.RumContextDataHandler
import com.datadog.android.sessionreplay.internal.recorder.SystemInformation
import com.datadog.android.sessionreplay.internal.recorder.base64.Base64LRUCache
import com.datadog.android.sessionreplay.internal.recorder.base64.Cache
import com.datadog.android.sessionreplay.internal.utils.TimeProvider
import com.datadog.android.sessionreplay.model.MobileSegment
import java.lang.ClassCastException
Expand All @@ -37,18 +34,15 @@ internal class RecordedDataQueueHandler {
private var processor: RecordedDataProcessor
private var rumContextDataHandler: RumContextDataHandler
private var timeProvider: TimeProvider
private var cache: Cache<Drawable, String>

internal constructor(
processor: RecordedDataProcessor,
rumContextDataHandler: RumContextDataHandler,
timeProvider: TimeProvider,
cache: Cache<Drawable, String> = Base64LRUCache
timeProvider: TimeProvider
) : this(
processor = processor,
rumContextDataHandler = rumContextDataHandler,
timeProvider = timeProvider,
cache = cache,

/**
* TODO: RUMM-0000 consider change to LoggingThreadPoolExecutor once V2 is merged.
Expand All @@ -70,14 +64,12 @@ internal class RecordedDataQueueHandler {
processor: RecordedDataProcessor,
rumContextDataHandler: RumContextDataHandler,
timeProvider: TimeProvider,
executorService: ExecutorService,
cache: Cache<Drawable, String>
executorService: ExecutorService
) {
this.processor = processor
this.rumContextDataHandler = rumContextDataHandler
this.executorService = executorService
this.timeProvider = timeProvider
this.cache = cache
}

// region internal
Expand Down Expand Up @@ -107,11 +99,6 @@ internal class RecordedDataQueueHandler {
val rumContextData = rumContextDataHandler.createRumContextData()
?: return null

// if the view changed then clear the drawable cache
if (rumContextData.prevRumContext != rumContextData.newRumContext) {
cache.clear()
}

val item = SnapshotRecordedDataQueueItem(
rumContextData = rumContextData,
systemInformation = systemInformation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,11 @@ import android.graphics.drawable.DrawableContainer
import android.graphics.drawable.LayerDrawable
import android.util.LruCache
import androidx.annotation.VisibleForTesting
import com.datadog.android.sessionreplay.internal.utils.CacheUtils

internal object Base64LRUCache : Cache<Drawable, String>, ComponentCallbacks2 {
@Suppress("MagicNumber")
val MAX_CACHE_MEMORY_SIZE_BYTES = 4 * 1024 * 1024 // 4MB
@Suppress("MagicNumber")
private val ON_LOW_MEMORY_SIZE_BYTES = MAX_CACHE_MEMORY_SIZE_BYTES / 2 // 50% size
@Suppress("MagicNumber")
private val ON_MODERATE_MEMORY_SIZE_BYTES = (MAX_CACHE_MEMORY_SIZE_BYTES / 4) * 3 // 75% size
private val MAX_CACHE_MEMORY_SIZE_BYTES = 4 * 1024 * 1024 // 4MB

private var cache: LruCache<String, ByteArray> = object :
LruCache<String, ByteArray>(MAX_CACHE_MEMORY_SIZE_BYTES) {
Expand All @@ -31,39 +28,8 @@ internal object Base64LRUCache : Cache<Drawable, String>, ComponentCallbacks2 {
}

override fun onTrimMemory(level: Int) {
when (level) {
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
cache.evictAll()
}

ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
cache.evictAll()
}

ComponentCallbacks2.TRIM_MEMORY_MODERATE -> {
cache.trimToSize(ON_MODERATE_MEMORY_SIZE_BYTES)
}

ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
cache.evictAll()
}

ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> {
cache.trimToSize(ON_LOW_MEMORY_SIZE_BYTES)
}

ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE -> {
cache.trimToSize(ON_MODERATE_MEMORY_SIZE_BYTES)
}

ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
cache.evictAll()
}

else -> {
cache.evictAll()
}
}
val cacheUtils = CacheUtils<String, ByteArray>()
cacheUtils.handleTrimMemory(level, cache)
}

override fun onConfigurationChanged(newConfig: Configuration) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.AsyncImageProcessingCallback
import com.datadog.android.sessionreplay.internal.recorder.base64.Cache.Companion.DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS
import com.datadog.android.sessionreplay.internal.utils.Base64Utils
import com.datadog.android.sessionreplay.internal.utils.DrawableUtils
import com.datadog.android.sessionreplay.model.MobileSegment
Expand All @@ -32,6 +33,7 @@ class Base64Serializer private constructor(
private val base64Utils: Base64Utils,
private val webPImageCompression: ImageCompression,
private val base64LruCache: Cache<Drawable, String>,
private val bitmapPool: Cache<String, Bitmap>,
private val logger: InternalLogger
) {
private var asyncImageProcessingCallback: AsyncImageProcessingCallback? = null
Expand All @@ -55,7 +57,11 @@ class Base64Serializer private constructor(
return
}

val bitmap = drawableUtils.createBitmapOfApproxSizeFromDrawable(drawable, displayMetrics)
val bitmap = drawableUtils.createBitmapOfApproxSizeFromDrawable(
applicationContext,
drawable,
displayMetrics
)

if (bitmap == null) {
asyncImageProcessingCallback?.finishProcessingImage()
Expand Down Expand Up @@ -118,19 +124,17 @@ class Base64Serializer private constructor(
private fun convertBmpToBase64(drawable: Drawable, bitmap: Bitmap): String {
val base64Result: String

val byteArrayOutputStream = webPImageCompression.compressBitmapToStream(bitmap)
val byteArray = webPImageCompression.compressBitmap(bitmap)

try {
base64Result = base64Utils.serializeToBase64String(byteArrayOutputStream)

if (base64Result.isNotEmpty()) {
// if we got a base64 string then cache it
base64LruCache.put(drawable, base64Result)
}
} finally {
bitmap.recycle()
base64Result = base64Utils.serializeToBase64String(byteArray)

if (base64Result.isNotEmpty()) {
// if we got a base64 string then cache it
base64LruCache.put(drawable, base64Result)
}

bitmapPool.put(bitmap)

return base64Result
}

Expand Down Expand Up @@ -169,6 +173,7 @@ class Base64Serializer private constructor(
base64Utils: Base64Utils = Base64Utils(),
webPImageCompression: ImageCompression = WebPImageCompression(),
base64LruCache: Cache<Drawable, String> = Base64LRUCache,
bitmapPool: Cache<String, Bitmap> = BitmapPool,
// Temporarily use UNBOUND until we handle the loggers
logger: InternalLogger = InternalLogger.UNBOUND
) =
Expand All @@ -178,6 +183,7 @@ class Base64Serializer private constructor(
base64Utils = base64Utils,
webPImageCompression = webPImageCompression,
base64LruCache = base64LruCache,
bitmapPool = bitmapPool,
logger = logger
)

Expand All @@ -201,11 +207,9 @@ class Base64Serializer private constructor(
// endregion

internal companion object {
internal const val DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS =
"Cache instance does not implement ComponentCallbacks2"

// The cache is a singleton, so we want to share this flag among
// all instances so that it's registered only once
private var isCacheRegisteredForCallbacks: Boolean = false
@VisibleForTesting
internal var isCacheRegisteredForCallbacks: Boolean = false
}
}
Loading