Skip to content

Commit

Permalink
Merge pull request #1564 from DataDog/jmoskovich/replay-1943/refactor…
Browse files Browse the repository at this point in the history
…-cache-singletons

REPLAY-1943: Refactor caches from singletons to class instances
  • Loading branch information
jonathanmos authored Aug 24, 2023
2 parents f1c9ce2 + 5be1cc0 commit 69f88d0
Show file tree
Hide file tree
Showing 17 changed files with 444 additions and 280 deletions.
2 changes: 2 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ datadog:
- "android.view.Window.Callback.onMenuItemSelected(kotlin.Int, android.view.MenuItem)"
- "android.view.View.getChildAt(kotlin.Int)"
- "android.view.View.hashCode()"
- "androidx.collection.LruCache.size()"
- "androidx.collection.LruCache.maxSize()"
# endregion
# region Android Webview APIs
- "android.webkit.ConsoleMessage.MessageLevel.toLogLevel()"
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 @@ -31,7 +31,7 @@ class com.datadog.android.sessionreplay.internal.recorder.base64.WebPImageCompre
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())
constructor(com.datadog.android.sessionreplay.utils.StringUtils = StringUtils, com.datadog.android.sessionreplay.utils.ViewUtils = ViewUtils)
protected fun resolveViewId(android.view.View): Long
protected fun colorAndAlphaAsStringHexa(Int, Int): String
protected fun resolveViewGlobalBounds(android.view.View, Float): com.datadog.android.sessionreplay.internal.recorder.GlobalBounds
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;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 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/BitmapPool;Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public abstract interface class com/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression {
Expand All @@ -108,12 +108,12 @@ public final class com/datadog/android/sessionreplay/internal/recorder/base64/We
public abstract class com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper : com/datadog/android/sessionreplay/internal/AsyncImageProcessingCallback, com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper {
public static final field Companion Lcom/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper$Companion;
public fun <init> ()V
public fun <init> (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;Lcom/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression;Lcom/datadog/android/sessionreplay/utils/UniqueIdentifierGenerator;Lcom/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer;)V
public synthetic fun <init> (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;Lcom/datadog/android/sessionreplay/internal/recorder/base64/ImageCompression;Lcom/datadog/android/sessionreplay/utils/UniqueIdentifierGenerator;Lcom/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;)V
public synthetic fun <init> (Lcom/datadog/android/sessionreplay/utils/StringUtils;Lcom/datadog/android/sessionreplay/utils/ViewUtils;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
protected final fun colorAndAlphaAsStringHexa (II)Ljava/lang/String;
public fun finishProcessingImage ()V
protected final fun getWebPMimeType ()Ljava/lang/String;
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 handleBitmap (Landroid/content/Context;Landroid/util/DisplayMetrics;Landroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe;)Lkotlin/Unit;
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
5 changes: 0 additions & 5 deletions features/dd-sdk-android-session-replay/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ plugins {
id("com.github.ben-manes.versions")

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

// Internal Generation
Expand Down Expand Up @@ -112,10 +111,6 @@ 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 @@ -20,6 +20,9 @@ import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toolbar
import androidx.appcompat.widget.SwitchCompat
import com.datadog.android.sessionreplay.internal.recorder.base64.Base64LRUCache
import com.datadog.android.sessionreplay.internal.recorder.base64.Base64Serializer
import com.datadog.android.sessionreplay.internal.recorder.base64.BitmapPool
import com.datadog.android.sessionreplay.internal.recorder.mapper.BasePickerMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.ButtonMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper
Expand Down Expand Up @@ -77,9 +80,11 @@ enum class SessionReplayPrivacy {

@Suppress("LongMethod")
internal fun mappers(): List<MapperTypeWrapper> {
val base64Serializer = buildBase64Serializer()

val viewWireframeMapper = ViewWireframeMapper()
val unsupportedViewMapper = UnsupportedViewMapper()
val imageButtonMapper = ImageButtonMapper()
val imageButtonMapper = ImageButtonMapper(base64Serializer = base64Serializer)
val imageMapper: ViewScreenshotWireframeMapper
val textMapper: TextViewMapper
val buttonMapper: ButtonMapper
Expand Down Expand Up @@ -161,6 +166,17 @@ enum class SessionReplayPrivacy {
return mappersList
}

private fun buildBase64Serializer(): Base64Serializer {
val bitmapPool = BitmapPool()
val base64LRUCache = Base64LRUCache()

val builder = Base64Serializer.Builder(
bitmapPool = bitmapPool,
base64LRUCache = base64LRUCache
)
return builder.build()
}

private fun getMaskSeekBarMapper(): MaskSeekBarWireframeMapper? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
MaskSeekBarWireframeMapper()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,74 @@ 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
import androidx.collection.LruCache
import com.datadog.android.sessionreplay.internal.utils.CacheUtils

internal object Base64LRUCache : Cache<Drawable, String>, ComponentCallbacks2 {
@Suppress("MagicNumber")
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) {
override fun sizeOf(key: String?, value: ByteArray): Int {
return value.size
import com.datadog.android.sessionreplay.internal.utils.InvocationUtils

internal class Base64LRUCache(
private val cacheUtils: CacheUtils<String, ByteArray> = CacheUtils(),
private val invocationUtils: InvocationUtils = InvocationUtils(),
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
}
}
}
) : Cache<Drawable, String>, ComponentCallbacks2 {

override fun onTrimMemory(level: Int) {
val cacheUtils = CacheUtils<String, ByteArray>()
cacheUtils.handleTrimMemory(level, cache)
}

override fun onConfigurationChanged(newConfig: Configuration) {}

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

@VisibleForTesting
internal fun setBackingCache(cache: LruCache<String, ByteArray>) {
this.cache = cache
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
invocationUtils.safeCallWithErrorLogging(
call = { cache.evictAll() },
failureMessage = FAILURE_MSG_EVICT_CACHE_CONTENTS
)
}

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

@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
invocationUtils.safeCallWithErrorLogging(
call = { cache.put(key, byteArray) },
failureMessage = FAILURE_MSG_PUT_CACHE
)
}

@Synchronized
override fun get(element: Drawable): String? =
cache.get(generateKey(element))?.let {
String(it)
}
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
invocationUtils.safeCallWithErrorLogging(
call = {
cache.get(generateKey(element))?.let {
String(it)
}
},
failureMessage = FAILURE_MSG_GET_CACHE
)

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

@Synchronized
override fun clear() {
cache.evictAll()
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
invocationUtils.safeCallWithErrorLogging(
call = { cache.evictAll() },
failureMessage = FAILURE_MSG_EVICT_CACHE_CONTENTS
)
}

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

private fun generatePrefix(drawable: Drawable): String {
Expand Down Expand Up @@ -97,4 +112,13 @@ internal object Base64LRUCache : Cache<Drawable, String>, ComponentCallbacks2 {
""
}
}

internal companion object {
@Suppress("MagicNumber")
internal val MAX_CACHE_MEMORY_SIZE_BYTES = 4 * 1024 * 1024 // 4MB

private const val FAILURE_MSG_EVICT_CACHE_CONTENTS = "Failed to evict cache entries"
private const val FAILURE_MSG_PUT_CACHE = "Failed to put item in cache"
private const val FAILURE_MSG_GET_CACHE = "Failed to get item from cache"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class Base64Serializer private constructor(
private val drawableUtils: DrawableUtils,
private val base64Utils: Base64Utils,
private val webPImageCompression: ImageCompression,
private val base64LruCache: Cache<Drawable, String>,
private val bitmapPool: Cache<String, Bitmap>,
private val base64LRUCache: Cache<Drawable, String>?,
private val bitmapPool: BitmapPool?,
private val logger: InternalLogger
) {
private var asyncImageProcessingCallback: AsyncImageProcessingCallback? = null
Expand All @@ -51,7 +51,7 @@ class Base64Serializer private constructor(

asyncImageProcessingCallback?.startProcessingImage()

val cachedBase64 = base64LruCache.get(drawable)
val cachedBase64 = base64LRUCache?.get(drawable)
if (cachedBase64 != null) {
finalizeRecordedDataItem(cachedBase64, imageWireframe, asyncImageProcessingCallback)
return
Expand Down Expand Up @@ -106,8 +106,8 @@ class Base64Serializer private constructor(
private fun registerCacheForCallbacks(applicationContext: Context) {
if (isCacheRegisteredForCallbacks) return

if (base64LruCache is ComponentCallbacks2) {
applicationContext.registerComponentCallbacks(base64LruCache)
if (base64LRUCache is ComponentCallbacks2) {
applicationContext.registerComponentCallbacks(base64LRUCache)
isCacheRegisteredForCallbacks = true
} else {
// Temporarily use UNBOUND logger
Expand All @@ -130,10 +130,10 @@ class Base64Serializer private constructor(

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

bitmapPool.put(bitmap)
bitmapPool?.put(bitmap)

return base64Result
}
Expand Down Expand Up @@ -166,34 +166,34 @@ class Base64Serializer private constructor(
// endregion

// region builder
internal class Builder {
internal fun build(
threadPoolExecutor: ExecutorService = THREADPOOL_EXECUTOR,
drawableUtils: DrawableUtils = DrawableUtils(),
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
) =
internal class Builder(
// Temporarily use UNBOUND until we handle the loggers
private var logger: InternalLogger = InternalLogger.UNBOUND,
private var threadPoolExecutor: ExecutorService = THREADPOOL_EXECUTOR,
private var bitmapPool: BitmapPool? = null,
private var base64LRUCache: Cache<Drawable, String>? = null,
private var drawableUtils: DrawableUtils = DrawableUtils(bitmapPool = bitmapPool),
private var base64Utils: Base64Utils = Base64Utils(),
private var webPImageCompression: ImageCompression = WebPImageCompression()
) {

internal fun build() =
Base64Serializer(
logger = logger,
threadPoolExecutor = threadPoolExecutor,
bitmapPool = bitmapPool,
base64LRUCache = base64LRUCache,
drawableUtils = drawableUtils,
base64Utils = base64Utils,
webPImageCompression = webPImageCompression,
base64LruCache = base64LruCache,
bitmapPool = bitmapPool,
logger = logger
webPImageCompression = webPImageCompression
)

private companion object {
private const val THREAD_POOL_MAX_KEEP_ALIVE_MS = 5000L
private const val CORE_DEFAULT_POOL_SIZE = 1
private const val MAX_THREAD_COUNT = 10

// all parameters are non-negative and queue is not null
@Suppress("UnsafeThirdPartyFunctionCall")
@Suppress("UnsafeThirdPartyFunctionCall") // all parameters are non-negative and queue is not null
private val THREADPOOL_EXECUTOR = ThreadPoolExecutor(
CORE_DEFAULT_POOL_SIZE,
MAX_THREAD_COUNT,
Expand Down
Loading

0 comments on commit 69f88d0

Please sign in to comment.