Skip to content

Commit

Permalink
Implement caching mechanism for base64
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Jul 19, 2023
1 parent b25e9a4 commit 257722b
Show file tree
Hide file tree
Showing 13 changed files with 632 additions and 49 deletions.
1 change: 1 addition & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ datadog:
- "java.lang.Object.constructor()"
- "java.lang.Runtime.availableProcessors()"
- "java.lang.Runtime.getRuntime()"
- "java.lang.Runtime.maxMemory()"
- "java.lang.System.currentTimeMillis()"
- "java.lang.System.getProperty(kotlin.String)"
- "java.lang.System.identityHashCode(kotlin.Any)"
Expand Down
2 changes: 1 addition & 1 deletion features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ abstract class com.datadog.android.sessionreplay.internal.recorder.mapper.BaseWi
protected fun android.graphics.drawable.Drawable.resolveShapeStyleAndBorder(Float): Pair<com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle?, com.datadog.android.sessionreplay.model.MobileSegment.ShapeBorder?>?
protected fun resolveChildDrawableUniqueIdentifier(android.view.View): Long?
protected fun getWebPMimeType(): String?
protected fun handleBitmap(android.util.DisplayMetrics, android.graphics.drawable.Drawable, com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ImageWireframe)
protected fun handleBitmap(android.content.Context, android.util.DisplayMetrics, android.graphics.drawable.Drawable, com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ImageWireframe)
override fun startProcessingImage()
override fun finishProcessingImage()
companion object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ 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;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/api/InternalLogger;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public abstract interface class com/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression {
Expand All @@ -113,7 +113,7 @@ public abstract class com/datadog/android/sessionreplay/internal/recorder/mapper
protected final fun colorAndAlphaAsStringHexa (II)Ljava/lang/String;
public fun finishProcessingImage ()V
protected final fun getWebPMimeType ()Ljava/lang/String;
protected final fun handleBitmap (Landroid/util/DisplayMetrics;Landroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe;)V
protected final fun handleBitmap (Landroid/content/Context;Landroid/util/DisplayMetrics;Landroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe;)V
protected final fun resolveChildDrawableUniqueIdentifier (Landroid/view/View;)Ljava/lang/Long;
protected final fun resolveShapeStyleAndBorder (Landroid/graphics/drawable/Drawable;F)Lkotlin/Pair;
protected final fun resolveViewGlobalBounds (Landroid/view/View;F)Lcom/datadog/android/sessionreplay/internal/recorder/GlobalBounds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

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 @@ -34,15 +37,18 @@ 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
timeProvider: TimeProvider,
cache: Cache<Drawable, String> = Base64LRUCache
) : this(
processor = processor,
rumContextDataHandler = rumContextDataHandler,
timeProvider = timeProvider,
cache = cache,

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

// region internal
Expand Down Expand Up @@ -99,6 +107,11 @@ 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
@@ -0,0 +1,134 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.internal.recorder.base64

import android.content.ComponentCallbacks2
import android.content.res.Configuration
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.DrawableContainer
import android.graphics.drawable.LayerDrawable
import android.util.LruCache
import androidx.annotation.VisibleForTesting

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 var cache: LruCache<String, ByteArray> = object :
LruCache<String, ByteArray>(MAX_CACHE_MEMORY_SIZE_BYTES) {
override fun sizeOf(key: String?, value: ByteArray): Int {
return value.size
}
}

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()
}
}
}

override fun onConfigurationChanged(newConfig: Configuration) {}

override fun onLowMemory() {
cache.evictAll()
}

@VisibleForTesting
internal fun setBackingCache(cache: LruCache<String, ByteArray>) {
this.cache = cache
}

@Synchronized
override fun put(element: Drawable, value: String) {
val key = generateKey(element)
val byteArray = value.toByteArray(Charsets.UTF_8)
cache.put(key, byteArray)
}

@Synchronized
override fun get(element: Drawable): String? =
cache.get(generateKey(element))?.let {
String(it)
}

@Synchronized
override fun size(): Int = cache.size()

@Synchronized
override fun clear() {
cache.evictAll()
}

private fun generateKey(drawable: Drawable): String =
generatePrefix(drawable) + System.identityHashCode(drawable)

private fun generatePrefix(drawable: Drawable): String {
return when (drawable) {
is DrawableContainer -> getPrefixForDrawableContainer(drawable)
is LayerDrawable -> getPrefixForLayerDrawable(drawable)
else -> ""
}
}

private fun getPrefixForDrawableContainer(drawable: DrawableContainer): String {
if (drawable !is AnimationDrawable) {
return drawable.state.joinToString(separator = "", postfix = "-")
}

return ""
}

private fun getPrefixForLayerDrawable(drawable: LayerDrawable): String {
return if (drawable.numberOfLayers > 1) {
val sb = StringBuilder()
for (index in 0 until drawable.numberOfLayers) {
val layer = drawable.getDrawable(index)
val layerHash = System.identityHashCode(layer).toString()
sb.append(layerHash)
sb.append("-")
}
"$sb"
} else {
""
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

package com.datadog.android.sessionreplay.internal.recorder.base64

import android.content.ComponentCallbacks2
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.util.DisplayMetrics
import androidx.annotation.MainThread
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.utils.Base64Utils
import com.datadog.android.sessionreplay.internal.utils.DrawableUtils
Expand All @@ -25,23 +28,33 @@ import java.util.concurrent.TimeUnit
@Suppress("UndocumentedPublicClass")
class Base64Serializer private constructor(
private val threadPoolExecutor: ExecutorService,
private val drawableUtils: DrawableUtils = DrawableUtils(),
private val base64Utils: Base64Utils = Base64Utils(),
private val webPImageCompression: ImageCompression = WebPImageCompression()
private val drawableUtils: DrawableUtils,
private val base64Utils: Base64Utils,
private val webPImageCompression: ImageCompression,
private val base64LruCache: Cache<Drawable, String>,
private val logger: InternalLogger
) {

private var asyncImageProcessingCallback: AsyncImageProcessingCallback? = null

// region internal

@MainThread
internal fun handleBitmap(
applicationContext: Context,
displayMetrics: DisplayMetrics,
drawable: Drawable,
imageWireframe: MobileSegment.Wireframe.ImageWireframe
) {
registerCacheForCallbacks(applicationContext)

asyncImageProcessingCallback?.startProcessingImage()

val cachedBase64 = base64LruCache.get(drawable)
if (cachedBase64 != null) {
finalizeRecordedDataItem(cachedBase64, imageWireframe, asyncImageProcessingCallback)
return
}

val bitmap = drawableUtils.createBitmapFromDrawable(drawable, displayMetrics)

if (bitmap == null) {
Expand All @@ -51,7 +64,7 @@ class Base64Serializer private constructor(

Runnable {
@Suppress("ThreadSafety") // this runs inside an executor
serialiseBitmap(bitmap, imageWireframe, asyncImageProcessingCallback)
serialiseBitmap(drawable, bitmap, imageWireframe, asyncImageProcessingCallback)
}.let { executeRunnable(it) }
}

Expand All @@ -78,30 +91,54 @@ class Base64Serializer private constructor(

@WorkerThread
private fun serialiseBitmap(
drawable: Drawable,
bitmap: Bitmap,
imageWireframe: MobileSegment.Wireframe.ImageWireframe,
asyncImageProcessingCallback: AsyncImageProcessingCallback?
) {
val base64String = convertBmpToBase64(bitmap)
val base64String = convertBmpToBase64(drawable, bitmap)
finalizeRecordedDataItem(base64String, imageWireframe, asyncImageProcessingCallback)
}

@MainThread
private fun registerCacheForCallbacks(applicationContext: Context) {
if (isCacheRegisteredForCallbacks) return

if (base64LruCache is ComponentCallbacks2) {
applicationContext.registerComponentCallbacks(base64LruCache)
isCacheRegisteredForCallbacks = true
} else {
// Temporarily use UNBOUND logger
// TODO: REPLAY-1364 Add logs here once the sdkLogger is added
logger.log(
level = InternalLogger.Level.WARN,
target = InternalLogger.Target.MAINTAINER,
messageBuilder = { DOES_NOT_IMPLEMENT_COMPONENTCALLBACKS }
)
}
}

@WorkerThread
private fun convertBmpToBase64(bitmap: Bitmap): String {
private fun convertBmpToBase64(drawable: Drawable, bitmap: Bitmap): String {
val byteArrayOutputStream = webPImageCompression.compressBitmapToStream(bitmap)

if (isOverSizeLimit(byteArrayOutputStream.size())) {
return ""
}

val base64: String
val base64String: String
try {
base64 = base64Utils.serializeToBase64String(byteArrayOutputStream)
base64String = base64Utils.serializeToBase64String(byteArrayOutputStream)

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

return base64
return base64String
}

private fun finalizeRecordedDataItem(
Expand Down Expand Up @@ -137,13 +174,18 @@ class Base64Serializer private constructor(
threadPoolExecutor: ExecutorService = THREADPOOL_EXECUTOR,
drawableUtils: DrawableUtils = DrawableUtils(),
base64Utils: Base64Utils = Base64Utils(),
webPImageCompression: ImageCompression = WebPImageCompression()
webPImageCompression: ImageCompression = WebPImageCompression(),
base64LruCache: Cache<Drawable, String> = Base64LRUCache,
// Temporarily use UNBOUND until we handle the loggers
logger: InternalLogger = InternalLogger.UNBOUND
) =
Base64Serializer(
threadPoolExecutor = threadPoolExecutor,
drawableUtils = drawableUtils,
base64Utils = base64Utils,
webPImageCompression = webPImageCompression
webPImageCompression = webPImageCompression,
base64LruCache = base64LruCache,
logger = logger
)

private companion object {
Expand All @@ -168,5 +210,12 @@ class Base64Serializer private constructor(
internal companion object {
@VisibleForTesting
internal const val BITMAP_SIZE_LIMIT_BYTES = 15000 // 15 kbs

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

0 comments on commit 257722b

Please sign in to comment.