Skip to content

Commit

Permalink
Implement pool of reusable bitmaps
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Jul 25, 2023
1 parent dcd6805 commit bd8e3f1
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
Expand All @@ -14,6 +15,7 @@ import com.datadog.android.sessionreplay.internal.processor.RecordedDataProcesso
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.BitmapPool
import com.datadog.android.sessionreplay.internal.recorder.base64.Cache
import com.datadog.android.sessionreplay.internal.utils.TimeProvider
import com.datadog.android.sessionreplay.model.MobileSegment
Expand All @@ -38,17 +40,20 @@ internal class RecordedDataQueueHandler {
private var rumContextDataHandler: RumContextDataHandler
private var timeProvider: TimeProvider
private var cache: Cache<Drawable, String>
private var bitmapPool: Cache<String, Bitmap>

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

/**
* TODO: RUMM-0000 consider change to LoggingThreadPoolExecutor once V2 is merged.
Expand All @@ -71,13 +76,15 @@ internal class RecordedDataQueueHandler {
rumContextDataHandler: RumContextDataHandler,
timeProvider: TimeProvider,
executorService: ExecutorService,
cache: Cache<Drawable, String>
cache: Cache<Drawable, String>,
bitmapPool: Cache<String, Bitmap>
) {
this.processor = processor
this.rumContextDataHandler = rumContextDataHandler
this.executorService = executorService
this.timeProvider = timeProvider
this.cache = cache
this.bitmapPool = bitmapPool
}

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

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

val item = SnapshotRecordedDataQueueItem(
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 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 @@ -55,7 +55,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 @@ -120,15 +124,11 @@ class Base64Serializer private constructor(

val byteArrayOutputStream = webPImageCompression.compressBitmapToStream(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(byteArrayOutputStream)

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

return base64Result
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.Bitmap
import android.graphics.Bitmap.Config
import android.util.LruCache
import com.datadog.android.sessionreplay.internal.utils.CacheUtils

internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {
@Suppress("MagicNumber")
val MAX_CACHE_MEMORY_SIZE_BYTES = (Runtime.getRuntime().maxMemory() / 8).toInt()

private var cache: LruCache<String, Bitmap> = object :
LruCache<String, Bitmap>(MAX_CACHE_MEMORY_SIZE_BYTES) {
override fun sizeOf(key: String?, value: Bitmap): Int {
return value.allocationByteCount
}
}

override fun put(value: Bitmap) {
val key = generateKey(value)
cache.put(key, value)
}

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

override fun clear() = cache.evictAll()

override fun get(element: String): Bitmap? = cache.get(element)

internal fun getBitmapByProperties(width: Int, height: Int, config: Config): Bitmap? {
val key = generateKey(width, height, config)
return this.get(key)
}

private fun generateKey(bitmap: Bitmap) =
generateKey(bitmap.width, bitmap.height, bitmap.config)

private fun generateKey(width: Int, height: Int, config: Config) =
"$width-$height-$config"

override fun onConfigurationChanged(newConfig: Configuration) {}

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

override fun onTrimMemory(level: Int) {
val cacheUtils = CacheUtils<String, Bitmap>()
cacheUtils.handleTrimMemory(level, cache)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
package com.datadog.android.sessionreplay.internal.recorder.base64

internal interface Cache<K : Any, V : Any> {
fun put(element: K, value: V)
fun get(element: K): V?

fun put(value: V) {}
fun put(element: K, value: V) {}
fun get(element: K): V? = null
fun size(): Int
fun clear()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.utils

import android.content.ComponentCallbacks2
import android.util.LruCache

internal class CacheUtils<K : Any, V : Any> {
internal fun handleTrimMemory(level: Int, cache: LruCache<K, V>) {
@Suppress("MagicNumber")
val onLowMemorySizeBytes = cache.maxSize() / 2

@Suppress("MagicNumber")
val onModerateMemorySizeBytes = cache.maxSize() / 4

when (level) {
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
cache.evictAll()
}

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

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

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

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

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

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

else -> {
cache.evictAll()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@

package com.datadog.android.sessionreplay.internal.utils

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.Config
import android.graphics.drawable.Drawable
import android.util.DisplayMetrics
import androidx.annotation.MainThread
import com.datadog.android.sessionreplay.internal.recorder.base64.BitmapPool
import com.datadog.android.sessionreplay.internal.recorder.wrappers.BitmapWrapper
import com.datadog.android.sessionreplay.internal.recorder.wrappers.CanvasWrapper
import kotlin.math.sqrt

internal class DrawableUtils(
private val bitmapWrapper: BitmapWrapper = BitmapWrapper(),
private val canvasWrapper: CanvasWrapper = CanvasWrapper()
private val canvasWrapper: CanvasWrapper = CanvasWrapper(),
private val bitmapPool: BitmapPool = BitmapPool
) {
/**
* This method attempts to create a bitmap from a drawable, such that the bitmap file size will
Expand All @@ -27,21 +31,19 @@ internal class DrawableUtils(
@MainThread
@Suppress("ReturnCount")
internal fun createBitmapOfApproxSizeFromDrawable(
applicationContext: Context,
drawable: Drawable,
displayMetrics: DisplayMetrics,
requestedSizeInBytes: Int = MAX_BITMAP_SIZE_IN_BYTES
requestedSizeInBytes: Int = MAX_BITMAP_SIZE_IN_BYTES,
config: Config = Config.ARGB_8888
): Bitmap? {
val (width, height) = getScaledWidthAndHeight(drawable, requestedSizeInBytes)
registerBitmapPoolForCallbacks(applicationContext)

val bitmap = bitmapWrapper.createBitmap(
displayMetrics,
width,
height,
Bitmap.Config.ARGB_8888
)
?: return null
val (width, height) = getScaledWidthAndHeight(drawable, requestedSizeInBytes)

val bitmap = getBitmapBySize(displayMetrics, width, height, config) ?: return null
val canvas = canvasWrapper.createCanvas(bitmap) ?: return null

drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
Expand Down Expand Up @@ -72,8 +74,52 @@ internal class DrawableUtils(
return Pair(width, height)
}

internal companion object {
internal const val MAX_BITMAP_SIZE_IN_BYTES = 10240 // 10kb
@Suppress("ReturnCount")
private fun getBitmapBySize(
displayMetrics: DisplayMetrics,
width: Int,
height: Int,
config: Config
): Bitmap? {
var bitmap = getReusableBitmapFromPool(width, height, config)
if (bitmap == null) {
bitmap = createNewBitmap(displayMetrics, width, height, config)
}
return bitmap
}

private fun getReusableBitmapFromPool(
width: Int,
height: Int,
config: Config
) = bitmapPool.getBitmapByProperties(width, height, config)

private fun createNewBitmap(
displayMetrics: DisplayMetrics,
width: Int,
height: Int,
config: Config
): Bitmap? {
val newBitmap = bitmapWrapper.createBitmap(displayMetrics, width, height, config)
newBitmap?.let {
bitmapPool.put(it)
}
return newBitmap
}

private fun registerBitmapPoolForCallbacks(applicationContext: Context) {
if (isBitmapPoolRegisteredForCallbacks) return

applicationContext.registerComponentCallbacks(bitmapPool)
isBitmapPoolRegisteredForCallbacks = true
}

private companion object {
private const val MAX_BITMAP_SIZE_IN_BYTES = 10240 // 10kb
private const val ARGB_8888_PIXEL_SIZE_BYTES = 4

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

0 comments on commit bd8e3f1

Please sign in to comment.