From d7ffd6584807a719831ce5f0f27c2183bf79b966 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Wed, 17 Jun 2020 00:44:04 +0200 Subject: [PATCH 01/13] improved performance in kotlin --- .../com/wolt/blurhashkt/BlurHashDecoder.kt | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index b3e7a37a..ab1f707a 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -2,13 +2,20 @@ package com.wolt.blurhashkt import android.graphics.Bitmap import android.graphics.Color -import kotlin.math.PI +import java.util.* import kotlin.math.cos import kotlin.math.pow import kotlin.math.withSign +// this is to optimize the number of calculations for "Math.cos()", +// is is slow and for many images with same size it can be cached, improving performance. +private const val USE_CACHE_FOR_MATH_COS = true + object BlurHashDecoder { + private val cacheCosinesX = HashMap() + private val cacheCosinesY = HashMap() + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { if (blurHash == null || blurHash.length < 6) { return null @@ -79,7 +86,18 @@ object BlurHashDecoder { numCompX: Int, numCompY: Int, colors: Array ): Bitmap { - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + // use an array for better performance when writing pixel colors + val imageArray = IntArray(width * height) + val calculateCosX = !USE_CACHE_FOR_MATH_COS || !cacheCosinesX.containsKey(width * numCompX) + val cosinesX: DoubleArray = getCosinesX(calculateCosX, width, numCompX) + val calculateCosY = !USE_CACHE_FOR_MATH_COS || !cacheCosinesY.containsKey(height * numCompY) + val cosinesY: DoubleArray + if (calculateCosY) { + cosinesY = DoubleArray(height * numCompY) + cacheCosinesY[height * numCompY] = cosinesY + } else { + cosinesY = cacheCosinesY[height * numCompY]!! + } for (y in 0 until height) { for (x in 0 until width) { var r = 0f @@ -87,17 +105,62 @@ object BlurHashDecoder { var b = 0f for (j in 0 until numCompY) { for (i in 0 until numCompX) { - val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat() + val cosX = getCosX(calculateCosX, cosinesX, i, numCompX, x, width) + val cosY = getCosY(calculateCosY, cosinesY, j, numCompY, y, height) + val basis = (cosX * cosY).toFloat() val color = colors[j * numCompX + i] r += color[0] * basis g += color[1] * basis b += color[2] * basis } } - bitmap.setPixel(x, y, Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))) + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun getCosY( + calculateCosY: Boolean, + cosinesY: DoubleArray, + j: Int, + numCompY: Int, + y: Int, + height: Int + ): Double { + if (calculateCosY) { + cosinesY[j + numCompY * y] = + cos(Math.PI * y * j / height) + } + val cosY = cosinesY[j + numCompY * y] + return cosY + } + + private fun getCosX( + calculateCosX: Boolean, + cosinesX: DoubleArray, + i: Int, + numCompX: Int, + x: Int, + width: Int + ): Double { + if (calculateCosX) { + cosinesX[i + numCompX * x] = + cos(Math.PI * x * i / width) + } + val cosX = cosinesX[i + numCompX * x] + return cosX + } + + private fun getCosinesX(calculateCosX: Boolean, width: Int, numCompX: Int): DoubleArray { + return when { + calculateCosX -> { + DoubleArray(width * numCompX).also { + cacheCosinesX[width * numCompX] = it + } } + else -> cacheCosinesX[width * numCompX]!! } - return bitmap } private fun linearToSrgb(value: Float): Int { @@ -109,13 +172,14 @@ object BlurHashDecoder { } } - private val charMap = listOf( + val listOfChars = listOf( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' ) + private val charMap = listOfChars .mapIndexed { i, c -> c to i } .toMap() From 4af91201f6d9d9bde3f30bea6f83216a94c72fc6 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Wed, 17 Jun 2020 00:48:43 +0200 Subject: [PATCH 02/13] small refactor --- .../com/wolt/blurhashkt/BlurHashDecoder.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index ab1f707a..5c594a84 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -91,13 +91,7 @@ object BlurHashDecoder { val calculateCosX = !USE_CACHE_FOR_MATH_COS || !cacheCosinesX.containsKey(width * numCompX) val cosinesX: DoubleArray = getCosinesX(calculateCosX, width, numCompX) val calculateCosY = !USE_CACHE_FOR_MATH_COS || !cacheCosinesY.containsKey(height * numCompY) - val cosinesY: DoubleArray - if (calculateCosY) { - cosinesY = DoubleArray(height * numCompY) - cacheCosinesY[height * numCompY] = cosinesY - } else { - cosinesY = cacheCosinesY[height * numCompY]!! - } + val cosinesY: DoubleArray = getCosinesY(calculateCosY, height, numCompY) for (y in 0 until height) { for (x in 0 until width) { var r = 0f @@ -120,6 +114,17 @@ object BlurHashDecoder { return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) } + private fun getCosinesY(calculateCosY: Boolean, height: Int, numCompY: Int): DoubleArray { + val cosinesY: DoubleArray + if (calculateCosY) { + cosinesY = DoubleArray(height * numCompY) + cacheCosinesY[height * numCompY] = cosinesY + } else { + cosinesY = cacheCosinesY[height * numCompY]!! + } + return cosinesY + } + private fun getCosY( calculateCosY: Boolean, cosinesY: DoubleArray, From 69c5b8bf3fd40b3242694b061719ce927f418264 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Wed, 17 Jun 2020 00:55:46 +0200 Subject: [PATCH 03/13] code cleanup --- .../com/wolt/blurhashkt/BlurHashDecoder.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index 5c594a84..9ee4b32f 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -89,9 +89,9 @@ object BlurHashDecoder { // use an array for better performance when writing pixel colors val imageArray = IntArray(width * height) val calculateCosX = !USE_CACHE_FOR_MATH_COS || !cacheCosinesX.containsKey(width * numCompX) - val cosinesX: DoubleArray = getCosinesX(calculateCosX, width, numCompX) + val cosinesX = getCosinesX(calculateCosX, width, numCompX) val calculateCosY = !USE_CACHE_FOR_MATH_COS || !cacheCosinesY.containsKey(height * numCompY) - val cosinesY: DoubleArray = getCosinesY(calculateCosY, height, numCompY) + val cosinesY = getCosinesY(calculateCosY, height, numCompY) for (y in 0 until height) { for (x in 0 until width) { var r = 0f @@ -134,11 +134,9 @@ object BlurHashDecoder { height: Int ): Double { if (calculateCosY) { - cosinesY[j + numCompY * y] = - cos(Math.PI * y * j / height) + cosinesY[j + numCompY * y] = cos(Math.PI * y * j / height) } - val cosY = cosinesY[j + numCompY * y] - return cosY + return cosinesY[j + numCompY * y] } private fun getCosX( @@ -150,11 +148,9 @@ object BlurHashDecoder { width: Int ): Double { if (calculateCosX) { - cosinesX[i + numCompX * x] = - cos(Math.PI * x * i / width) + cosinesX[i + numCompX * x] = cos(Math.PI * x * i / width) } - val cosX = cosinesX[i + numCompX * x] - return cosX + return cosinesX[i + numCompX * x] } private fun getCosinesX(calculateCosX: Boolean, width: Int, numCompX: Int): DoubleArray { @@ -177,14 +173,13 @@ object BlurHashDecoder { } } - val listOfChars = listOf( + private val charMap = listOf( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' ) - private val charMap = listOfChars .mapIndexed { i, c -> c to i } .toMap() From c4aab797583a76f0dd1a61c9141c0bcf266be40e Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Wed, 17 Jun 2020 01:10:10 +0200 Subject: [PATCH 04/13] added decode time in demo screen --- .../src/main/java/com/wolt/blurhashapp/MainActivity.kt | 6 +++++- Kotlin/demo/src/main/res/layout/activity_main.xml | 9 +++++++++ .../src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index 274b058a..0e49fa5f 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -1,12 +1,13 @@ package com.wolt.blurhashapp import android.os.Bundle +import android.os.SystemClock import android.view.View import android.widget.EditText import android.widget.ImageView -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.wolt.blurhashkt.BlurHashDecoder +import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { @@ -16,8 +17,11 @@ class MainActivity : AppCompatActivity() { val etInput: EditText = findViewById(R.id.etInput) val ivResult: ImageView = findViewById(R.id.ivResult) findViewById(R.id.tvDecode).setOnClickListener { + val start = SystemClock.elapsedRealtime() val bitmap = BlurHashDecoder.decode(etInput.text.toString(), 20, 12) + val time = SystemClock.elapsedRealtime() - start ivResult.setImageBitmap(bitmap) + ivResultMs.text = "Decode time: $time ms" } } diff --git a/Kotlin/demo/src/main/res/layout/activity_main.xml b/Kotlin/demo/src/main/res/layout/activity_main.xml index 84249e62..664d1e27 100644 --- a/Kotlin/demo/src/main/res/layout/activity_main.xml +++ b/Kotlin/demo/src/main/res/layout/activity_main.xml @@ -1,5 +1,6 @@ + + diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index 9ee4b32f..eb279e27 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -9,6 +9,7 @@ import kotlin.math.withSign // this is to optimize the number of calculations for "Math.cos()", // is is slow and for many images with same size it can be cached, improving performance. +// the improvement can be noticed with images bigger than 80x80 private const val USE_CACHE_FOR_MATH_COS = true object BlurHashDecoder { From aad3f64a9623c3e945cf181cec514c9d10724d83 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Wed, 17 Jun 2020 14:07:28 +0200 Subject: [PATCH 05/13] added quick benchmark to play a bit. Will remove it later. --- Kotlin/demo/build.gradle | 2 + .../java/com/wolt/blurhashapp/MainActivity.kt | 110 ++++++++++++++++-- .../src/main/res/layout/activity_main.xml | 22 +++- .../com/wolt/blurhashkt/BlurHashDecoder.kt | 56 +++++++-- 4 files changed, 165 insertions(+), 25 deletions(-) diff --git a/Kotlin/demo/build.gradle b/Kotlin/demo/build.gradle index 6ccb0378..f4355af1 100644 --- a/Kotlin/demo/build.gradle +++ b/Kotlin/demo/build.gradle @@ -26,4 +26,6 @@ android { dependencies { implementation project(path: ':lib') implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' } diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index 0e49fa5f..eb59bbe0 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -1,28 +1,120 @@ package com.wolt.blurhashapp +import android.graphics.Bitmap import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.os.SystemClock -import android.view.View -import android.widget.EditText -import android.widget.ImageView +import android.view.View.INVISIBLE +import android.view.View.VISIBLE import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.* import com.wolt.blurhashkt.BlurHashDecoder import kotlinx.android.synthetic.main.activity_main.* +import java.util.concurrent.Executors +import kotlin.math.pow class MainActivity : AppCompatActivity() { + private lateinit var vm: Vm + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - val etInput: EditText = findViewById(R.id.etInput) - val ivResult: ImageView = findViewById(R.id.ivResult) - findViewById(R.id.tvDecode).setOnClickListener { - val start = SystemClock.elapsedRealtime() + vm = ViewModelProvider(this).get(Vm::class.java) + vm.observe(this, Observer { + when (it) { + "START" -> progressBar.visibility = VISIBLE + "END" -> progressBar.visibility = INVISIBLE + else -> { + ivResultBenchmark.append("\n$it") + ivResultBenchmark.scrollTo(0, ivResultBenchmark.layout.lineCount) + } + } + }) + tvDecode.setOnClickListener { val bitmap = BlurHashDecoder.decode(etInput.text.toString(), 20, 12) - val time = SystemClock.elapsedRealtime() - start ivResult.setImageBitmap(bitmap) - ivResultMs.text = "Decode time: $time ms" + ivResultBenchmark.setText("") + vm.startBenchMark(etInput.text.toString()) + } + } + +} + +/** + * Executes a function and return the time spent in milliseconds. + */ +private inline fun timed(function: () -> Unit): Long { + val start = SystemClock.elapsedRealtime() + function() + return SystemClock.elapsedRealtime() - start +} + +class Vm : ViewModel() { + private val liveData = MutableLiveData() + private val executor = Executors.newSingleThreadExecutor() + private val handler = Handler(Looper.getMainLooper()) + + fun observe(owner: LifecycleOwner, observer: Observer) { + liveData.observe(owner, observer) + } + + fun startBenchMark(blurHash: String) { + executor.execute { + notifyBenchmark("START") + } + for (useArray in 1 downTo 0) { + val useArray1 = useArray == 1 + for (useCache in 1 downTo 0) { + val useCache1 = useCache == 1 + executor.execute { + notifyBenchmark("-----------------------------------") + notifyBenchmark("Array: $useArray1, cache: $useCache1") + notifyBenchmark("-----------------------------------") + } + for (s in 1..3) { + val width = 20 * 2.toDouble().pow(s - 1).toInt() + val height = 12 * 2.toDouble().pow(s - 1).toInt() + executor.execute { + notifyBenchmark("width: $width, height: $height") + } + for (n in 1..3) { + executor.execute { + benchmark(10.toDouble().pow(n).toInt(), width, height, blurHash, useArray1, useCache1) + } + } + executor.execute { + notifyBenchmark("\n") + } + } + val s = "-----------------------------------\n" + executor.execute { + notifyBenchmark(s) + } + } + } + executor.execute { + notifyBenchmark("END") } } + private fun benchmark(max: Int, width: Int, height: Int, blurHash: String, useArray: Boolean, useCache: Boolean) { + notifyBenchmark("-> $max bitmaps") + var bmp: Bitmap? = null + val time = timed { + for (i in 1..max) { + bmp = BlurHashDecoder.decode(blurHash, width, height, useArray = useArray, useCache = useCache) + } + } + notifyBenchmark("<- $time ms, Avg: ${time / max.toDouble()} ms") + // log the bitmap size + println("bmp size: ${bmp?.byteCount}") + } + + private fun notifyBenchmark(s: String) { + handler.post { + liveData.value = s + } + } } diff --git a/Kotlin/demo/src/main/res/layout/activity_main.xml b/Kotlin/demo/src/main/res/layout/activity_main.xml index 664d1e27..c5557ac0 100644 --- a/Kotlin/demo/src/main/res/layout/activity_main.xml +++ b/Kotlin/demo/src/main/res/layout/activity_main.xml @@ -41,12 +41,26 @@ android:layout_marginTop="24dp" android:adjustViewBounds="true" /> - + + + diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index eb279e27..b43094b7 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -2,22 +2,17 @@ package com.wolt.blurhashkt import android.graphics.Bitmap import android.graphics.Color -import java.util.* import kotlin.math.cos import kotlin.math.pow import kotlin.math.withSign -// this is to optimize the number of calculations for "Math.cos()", -// is is slow and for many images with same size it can be cached, improving performance. -// the improvement can be noticed with images bigger than 80x80 -private const val USE_CACHE_FOR_MATH_COS = true - object BlurHashDecoder { private val cacheCosinesX = HashMap() private val cacheCosinesY = HashMap() - fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useArray: Boolean = true, useCache: Boolean = true): Bitmap? { + if (blurHash == null || blurHash.length < 6) { return null } @@ -39,7 +34,10 @@ object BlurHashDecoder { decodeAc(colorEnc, maxAc * punch) } } - return composeBitmap(width, height, numCompX, numCompY, colors) + if (useArray) + return composeBitmapArray(width, height, numCompX, numCompY, colors, useCache) + else + return composeBitmap(width, height, numCompX, numCompY, colors, useCache) } private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { @@ -82,16 +80,17 @@ object BlurHashDecoder { private fun signedPow2(value: Float) = value.pow(2f).withSign(value) - private fun composeBitmap( + private fun composeBitmapArray( width: Int, height: Int, numCompX: Int, numCompY: Int, - colors: Array + colors: Array, + useCache: Boolean ): Bitmap { // use an array for better performance when writing pixel colors val imageArray = IntArray(width * height) - val calculateCosX = !USE_CACHE_FOR_MATH_COS || !cacheCosinesX.containsKey(width * numCompX) + val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) val cosinesX = getCosinesX(calculateCosX, width, numCompX) - val calculateCosY = !USE_CACHE_FOR_MATH_COS || !cacheCosinesY.containsKey(height * numCompY) + val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) val cosinesY = getCosinesY(calculateCosY, height, numCompY) for (y in 0 until height) { for (x in 0 until width) { @@ -115,6 +114,39 @@ object BlurHashDecoder { return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) } + private fun composeBitmap( + width: Int, height: Int, + numCompX: Int, numCompY: Int, + colors: Array, + useCache: Boolean + ): Bitmap { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) + val cosinesX = getCosinesX(calculateCosX, width, numCompX) + val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) + val cosinesY = getCosinesY(calculateCosY, height, numCompY) + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + for (i in 0 until numCompX) { + val cosX = getCosX(calculateCosX, cosinesX, i, numCompX, x, width) + val cosY = getCosY(calculateCosY, cosinesY, j, numCompY, y, height) + val basis = (cosX * cosY).toFloat() + val color = colors[j * numCompX + i] + r += color[0] * basis + g += color[1] * basis + b += color[2] * basis + } + } + bitmap.setPixel(x, y, Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))) + } + } + return bitmap + } + private fun getCosinesY(calculateCosY: Boolean, height: Int, numCompY: Int): DoubleArray { val cosinesY: DoubleArray if (calculateCosY) { From 9dfe400ce03445e35acad65e40e30ce72cc492ef Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Wed, 17 Jun 2020 15:21:16 +0200 Subject: [PATCH 06/13] clear cache before each benchmark starts --- .../java/com/wolt/blurhashapp/MainActivity.kt | 15 +++++++++------ .../java/com/wolt/blurhashkt/BlurHashDecoder.kt | 5 +++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index eb59bbe0..72f586e0 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -1,10 +1,7 @@ package com.wolt.blurhashapp import android.graphics.Bitmap -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.SystemClock +import android.os.* import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AppCompatActivity @@ -64,6 +61,11 @@ class Vm : ViewModel() { executor.execute { notifyBenchmark("START") } + executor.execute { + notifyBenchmark("-----------------------------------") + notifyBenchmark("Device: ${Build.MANUFACTURER} - ${Build.MODEL}") + notifyBenchmark("OS: Android ${Build.VERSION.CODENAME} - API ${Build.VERSION.SDK_INT}") + } for (useArray in 1 downTo 0) { val useArray1 = useArray == 1 for (useCache in 1 downTo 0) { @@ -74,8 +76,8 @@ class Vm : ViewModel() { notifyBenchmark("-----------------------------------") } for (s in 1..3) { - val width = 20 * 2.toDouble().pow(s - 1).toInt() - val height = 12 * 2.toDouble().pow(s - 1).toInt() + val width = 20 * 2.0.pow(s - 1).toInt() + val height = 12 * 2.0.pow(s - 1).toInt() executor.execute { notifyBenchmark("width: $width, height: $height") } @@ -102,6 +104,7 @@ class Vm : ViewModel() { private fun benchmark(max: Int, width: Int, height: Int, blurHash: String, useArray: Boolean, useCache: Boolean) { notifyBenchmark("-> $max bitmaps") var bmp: Bitmap? = null + BlurHashDecoder.clearCache() val time = timed { for (i in 1..max) { bmp = BlurHashDecoder.decode(blurHash, width, height, useArray = useArray, useCache = useCache) diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index b43094b7..cc649e13 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -11,6 +11,11 @@ object BlurHashDecoder { private val cacheCosinesX = HashMap() private val cacheCosinesY = HashMap() + fun clearCache() { + cacheCosinesX.clear() + cacheCosinesY.clear() + } + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useArray: Boolean = true, useCache: Boolean = true): Bitmap? { if (blurHash == null || blurHash.length < 6) { From 08e71c8eaa63b9bdb9740707fa9dc0d11a0e7944 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Wed, 17 Jun 2020 20:32:33 +0200 Subject: [PATCH 07/13] coroutines in kotlin for parallel calculation. Higher performance for bigger images --- .../java/com/wolt/blurhashapp/MainActivity.kt | 48 ++++----- Kotlin/lib/build.gradle | 5 + .../com/wolt/blurhashkt/BlurHashDecoder.kt | 97 +++++++++---------- 3 files changed, 72 insertions(+), 78 deletions(-) diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index 72f586e0..4ceeff53 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -66,48 +66,40 @@ class Vm : ViewModel() { notifyBenchmark("Device: ${Build.MANUFACTURER} - ${Build.MODEL}") notifyBenchmark("OS: Android ${Build.VERSION.CODENAME} - API ${Build.VERSION.SDK_INT}") } - for (useArray in 1 downTo 0) { - val useArray1 = useArray == 1 - for (useCache in 1 downTo 0) { - val useCache1 = useCache == 1 - executor.execute { - notifyBenchmark("-----------------------------------") - notifyBenchmark("Array: $useArray1, cache: $useCache1") - notifyBenchmark("-----------------------------------") - } - for (s in 1..3) { - val width = 20 * 2.0.pow(s - 1).toInt() - val height = 12 * 2.0.pow(s - 1).toInt() - executor.execute { - notifyBenchmark("width: $width, height: $height") - } - for (n in 1..3) { - executor.execute { - benchmark(10.toDouble().pow(n).toInt(), width, height, blurHash, useArray1, useCache1) - } - } - executor.execute { - notifyBenchmark("\n") - } - } - val s = "-----------------------------------\n" + executor.execute { + notifyBenchmark("-----------------------------------") + } + for (s in 1..3) { + val width = 20 * 2.0.pow(s - 1).toInt() + val height = 12 * 2.0.pow(s - 1).toInt() + executor.execute { + notifyBenchmark("width: $width, height: $height") + } + for (n in 0..3) { executor.execute { - notifyBenchmark(s) + benchmark(10.0.pow(n).toInt(), width, height, blurHash) } } + executor.execute { + notifyBenchmark("\n") + } + } + val s = "-----------------------------------\n" + executor.execute { + notifyBenchmark(s) } executor.execute { notifyBenchmark("END") } } - private fun benchmark(max: Int, width: Int, height: Int, blurHash: String, useArray: Boolean, useCache: Boolean) { + private fun benchmark(max: Int, width: Int, height: Int, blurHash: String, useCache: Boolean = true) { notifyBenchmark("-> $max bitmaps") var bmp: Bitmap? = null BlurHashDecoder.clearCache() val time = timed { for (i in 1..max) { - bmp = BlurHashDecoder.decode(blurHash, width, height, useArray = useArray, useCache = useCache) + bmp = BlurHashDecoder.decode(blurHash, width, height, useCache = useCache) } } notifyBenchmark("<- $time ms, Avg: ${time / max.toDouble()} ms") diff --git a/Kotlin/lib/build.gradle b/Kotlin/lib/build.gradle index 939e8b38..61f27f1e 100644 --- a/Kotlin/lib/build.gradle +++ b/Kotlin/lib/build.gradle @@ -27,4 +27,9 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7' + + androidTestImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test:runner:1.2.0' } diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index cc649e13..99529041 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -2,10 +2,14 @@ package com.wolt.blurhashkt import android.graphics.Bitmap import android.graphics.Color +import kotlinx.coroutines.* import kotlin.math.cos import kotlin.math.pow import kotlin.math.withSign +private const val NUMBER_OF_PARALLEL_TASKS = 3 +private val COROUTINES_SCOPE_FOR_PARALLEL_TASKS = GlobalScope + object BlurHashDecoder { private val cacheCosinesX = HashMap() @@ -16,7 +20,7 @@ object BlurHashDecoder { cacheCosinesY.clear() } - fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useArray: Boolean = true, useCache: Boolean = true): Bitmap? { + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? { if (blurHash == null || blurHash.length < 6) { return null @@ -39,10 +43,7 @@ object BlurHashDecoder { decodeAc(colorEnc, maxAc * punch) } } - if (useArray) - return composeBitmapArray(width, height, numCompX, numCompY, colors, useCache) - else - return composeBitmap(width, height, numCompX, numCompY, colors, useCache) + return composeBitmap(width, height, numCompX, numCompY, colors, useCache) } private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { @@ -85,7 +86,7 @@ object BlurHashDecoder { private fun signedPow2(value: Float) = value.pow(2f).withSign(value) - private fun composeBitmapArray( + private fun composeBitmap( width: Int, height: Int, numCompX: Int, numCompY: Int, colors: Array, @@ -97,59 +98,55 @@ object BlurHashDecoder { val cosinesX = getCosinesX(calculateCosX, width, numCompX) val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) val cosinesY = getCosinesY(calculateCosY, height, numCompY) - for (y in 0 until height) { - for (x in 0 until width) { - var r = 0f - var g = 0f - var b = 0f - for (j in 0 until numCompY) { - for (i in 0 until numCompX) { - val cosX = getCosX(calculateCosX, cosinesX, i, numCompX, x, width) - val cosY = getCosY(calculateCosY, cosinesY, j, numCompY, y, height) - val basis = (cosX * cosY).toFloat() - val color = colors[j * numCompX + i] - r += color[0] * basis - g += color[1] * basis - b += color[2] * basis - } + runBlocking { + COROUTINES_SCOPE_FOR_PARALLEL_TASKS.launch { + val tasks = ArrayList>() + val step = height / NUMBER_OF_PARALLEL_TASKS + for (t in 0 until NUMBER_OF_PARALLEL_TASKS) { + val start = step * t + tasks.add(async { + for (y in start until start + step) { + compositBitmapOnlyX(width, numCompY, numCompX, calculateCosX, cosinesX, calculateCosY, cosinesY, y, height, colors, imageArray) + } + return@async + }) } - imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) - } + tasks.forEach { it.await() } + }.join() } return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) } - private fun composeBitmap( - width: Int, height: Int, - numCompX: Int, numCompY: Int, + private fun compositBitmapOnlyX( + width: Int, + numCompY: Int, + numCompX: Int, + calculateCosX: Boolean, + cosinesX: DoubleArray, + calculateCosY: Boolean, + cosinesY: DoubleArray, + y: Int, + height: Int, colors: Array, - useCache: Boolean - ): Bitmap { - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) - val cosinesX = getCosinesX(calculateCosX, width, numCompX) - val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) - val cosinesY = getCosinesY(calculateCosY, height, numCompY) - for (y in 0 until height) { - for (x in 0 until width) { - var r = 0f - var g = 0f - var b = 0f - for (j in 0 until numCompY) { - for (i in 0 until numCompX) { - val cosX = getCosX(calculateCosX, cosinesX, i, numCompX, x, width) - val cosY = getCosY(calculateCosY, cosinesY, j, numCompY, y, height) - val basis = (cosX * cosY).toFloat() - val color = colors[j * numCompX + i] - r += color[0] * basis - g += color[1] * basis - b += color[2] * basis - } + imageArray: IntArray + ) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + for (i in 0 until numCompX) { + val cosX = getCosX(calculateCosX, cosinesX, i, numCompX, x, width) + val cosY = getCosY(calculateCosY, cosinesY, j, numCompY, y, height) + val basis = (cosX * cosY).toFloat() + val color = colors[j * numCompX + i] + r += color[0] * basis + g += color[1] * basis + b += color[2] * basis } - bitmap.setPixel(x, y, Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))) } + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) } - return bitmap } private fun getCosinesY(calculateCosY: Boolean, height: Int, numCompY: Int): DoubleArray { From 504dbad1172bd42a0668512a843adb05f29a4dcf Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Tue, 23 Jun 2020 13:06:24 +0200 Subject: [PATCH 08/13] added number of parallel tasks to benchmark --- .../java/com/wolt/blurhashapp/MainActivity.kt | 30 ++++++++++++------- .../com/wolt/blurhashkt/BlurHashDecoder.kt | 12 ++++---- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index 4ceeff53..f9cb4f80 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -69,19 +69,27 @@ class Vm : ViewModel() { executor.execute { notifyBenchmark("-----------------------------------") } - for (s in 1..3) { - val width = 20 * 2.0.pow(s - 1).toInt() - val height = 12 * 2.0.pow(s - 1).toInt() + for (tasks in 1..3) { executor.execute { - notifyBenchmark("width: $width, height: $height") + notifyBenchmark("") + notifyBenchmark("-----------------------------------") + notifyBenchmark("Parallel tasks: $tasks") + notifyBenchmark("-----------------------------------") } - for (n in 0..3) { + for (size in 1..3) { + val width = 20 * 2.0.pow(size - 1).toInt() + val height = 12 * 2.0.pow(size - 1).toInt() executor.execute { - benchmark(10.0.pow(n).toInt(), width, height, blurHash) + notifyBenchmark("width: $width, height: $height") + } + for (n in 0..3) { + executor.execute { + benchmark(10.0.pow(n).toInt(), width, height, blurHash, useCache = true, tasks = tasks) + } + } + executor.execute { + notifyBenchmark("\n") } - } - executor.execute { - notifyBenchmark("\n") } } val s = "-----------------------------------\n" @@ -93,13 +101,13 @@ class Vm : ViewModel() { } } - private fun benchmark(max: Int, width: Int, height: Int, blurHash: String, useCache: Boolean = true) { + private fun benchmark(max: Int, width: Int, height: Int, blurHash: String, useCache: Boolean, tasks: Int) { notifyBenchmark("-> $max bitmaps") var bmp: Bitmap? = null BlurHashDecoder.clearCache() val time = timed { for (i in 1..max) { - bmp = BlurHashDecoder.decode(blurHash, width, height, useCache = useCache) + bmp = BlurHashDecoder.decode(blurHash, width, height, useCache = useCache, parallelTasks = tasks) } } notifyBenchmark("<- $time ms, Avg: ${time / max.toDouble()} ms") diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index 99529041..a597638c 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -7,7 +7,6 @@ import kotlin.math.cos import kotlin.math.pow import kotlin.math.withSign -private const val NUMBER_OF_PARALLEL_TASKS = 3 private val COROUTINES_SCOPE_FOR_PARALLEL_TASKS = GlobalScope object BlurHashDecoder { @@ -20,7 +19,7 @@ object BlurHashDecoder { cacheCosinesY.clear() } - fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? { + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true, parallelTasks: Int = 3): Bitmap? { if (blurHash == null || blurHash.length < 6) { return null @@ -43,7 +42,7 @@ object BlurHashDecoder { decodeAc(colorEnc, maxAc * punch) } } - return composeBitmap(width, height, numCompX, numCompY, colors, useCache) + return composeBitmap(width, height, numCompX, numCompY, colors, useCache, parallelTasks) } private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { @@ -90,7 +89,8 @@ object BlurHashDecoder { width: Int, height: Int, numCompX: Int, numCompY: Int, colors: Array, - useCache: Boolean + useCache: Boolean, + parallelTasks: Int ): Bitmap { // use an array for better performance when writing pixel colors val imageArray = IntArray(width * height) @@ -101,8 +101,8 @@ object BlurHashDecoder { runBlocking { COROUTINES_SCOPE_FOR_PARALLEL_TASKS.launch { val tasks = ArrayList>() - val step = height / NUMBER_OF_PARALLEL_TASKS - for (t in 0 until NUMBER_OF_PARALLEL_TASKS) { + val step = height / parallelTasks + for (t in 0 until parallelTasks) { val start = step * t tasks.add(async { for (y in start until start + step) { From 5e93b877e43faa8998ea7cbe58dbbec76cc3b4a4 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Tue, 23 Jun 2020 13:28:42 +0200 Subject: [PATCH 09/13] last commit. Proof that coroutines do not improve performance, it is the opposite. What happens is the performance of multiple parallel tasks is better than 1 single task. But the performance without using coroutines is better than 1 task using coroutines. Maybe there is a way to use parallel tasks in an optimal way without using coroutines, but not sure the performance will increase too much. --- .../java/com/wolt/blurhashapp/MainActivity.kt | 4 +- .../com/wolt/blurhashkt/BlurHashDecoder.kt | 104 ++++++++++-------- 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index f9cb4f80..1d061a2a 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -69,14 +69,14 @@ class Vm : ViewModel() { executor.execute { notifyBenchmark("-----------------------------------") } - for (tasks in 1..3) { + for (tasks in 1..6) { executor.execute { notifyBenchmark("") notifyBenchmark("-----------------------------------") notifyBenchmark("Parallel tasks: $tasks") notifyBenchmark("-----------------------------------") } - for (size in 1..3) { + for (size in 1..1) { val width = 20 * 2.0.pow(size - 1).toInt() val height = 12 * 2.0.pow(size - 1).toInt() executor.execute { diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index a597638c..653cb05b 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -42,7 +42,10 @@ object BlurHashDecoder { decodeAc(colorEnc, maxAc * punch) } } - return composeBitmap(width, height, numCompX, numCompY, colors, useCache, parallelTasks) + return when (parallelTasks) { + 1 -> composeBitmap(width, height, numCompX, numCompY, colors, useCache) + else -> composeBitmapCoroutines(width, height, numCompX, numCompY, colors, useCache, parallelTasks) + } } private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { @@ -86,6 +89,40 @@ object BlurHashDecoder { private fun signedPow2(value: Float) = value.pow(2f).withSign(value) private fun composeBitmap( + width: Int, height: Int, + numCompX: Int, numCompY: Int, + colors: Array, + useCache: Boolean + ): Bitmap { + // use an array for better performance when writing pixel colors + val imageArray = IntArray(width * height) + val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) + val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX) + val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) + val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY) + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + for (i in 0 until numCompX) { + val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width) + val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height) + val basis = (cosX * cosY).toFloat() + val color = colors[j * numCompX + i] + r += color[0] * basis + g += color[1] * basis + b += color[2] * basis + } + } + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun composeBitmapCoroutines( width: Int, height: Int, numCompX: Int, numCompY: Int, colors: Array, @@ -95,9 +132,9 @@ object BlurHashDecoder { // use an array for better performance when writing pixel colors val imageArray = IntArray(width * height) val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) - val cosinesX = getCosinesX(calculateCosX, width, numCompX) + val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX) val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) - val cosinesY = getCosinesY(calculateCosY, height, numCompY) + val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY) runBlocking { COROUTINES_SCOPE_FOR_PARALLEL_TASKS.launch { val tasks = ArrayList>() @@ -136,8 +173,8 @@ object BlurHashDecoder { var b = 0f for (j in 0 until numCompY) { for (i in 0 until numCompX) { - val cosX = getCosX(calculateCosX, cosinesX, i, numCompX, x, width) - val cosY = getCosY(calculateCosY, cosinesY, j, numCompY, y, height) + val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width) + val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height) val basis = (cosX * cosY).toFloat() val color = colors[j * numCompX + i] r += color[0] * basis @@ -149,54 +186,31 @@ object BlurHashDecoder { } } - private fun getCosinesY(calculateCosY: Boolean, height: Int, numCompY: Int): DoubleArray { - val cosinesY: DoubleArray - if (calculateCosY) { - cosinesY = DoubleArray(height * numCompY) - cacheCosinesY[height * numCompY] = cosinesY - } else { - cosinesY = cacheCosinesY[height * numCompY]!! + private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when { + calculate -> { + DoubleArray(height * numCompY).also { + cacheCosinesY[height * numCompY] = it + } } - return cosinesY - } - - private fun getCosY( - calculateCosY: Boolean, - cosinesY: DoubleArray, - j: Int, - numCompY: Int, - y: Int, - height: Int - ): Double { - if (calculateCosY) { - cosinesY[j + numCompY * y] = cos(Math.PI * y * j / height) + else -> { + cacheCosinesY[height * numCompY]!! } - return cosinesY[j + numCompY * y] } - private fun getCosX( - calculateCosX: Boolean, - cosinesX: DoubleArray, - i: Int, - numCompX: Int, - x: Int, - width: Int - ): Double { - if (calculateCosX) { - cosinesX[i + numCompX * x] = cos(Math.PI * x * i / width) + private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when { + calculate -> { + DoubleArray(width * numCompX).also { + cacheCosinesX[width * numCompX] = it + } } - return cosinesX[i + numCompX * x] + else -> cacheCosinesX[width * numCompX]!! } - private fun getCosinesX(calculateCosX: Boolean, width: Int, numCompX: Int): DoubleArray { - return when { - calculateCosX -> { - DoubleArray(width * numCompX).also { - cacheCosinesX[width * numCompX] = it - } - } - else -> cacheCosinesX[width * numCompX]!! + private fun DoubleArray.getCos(calculate: Boolean, x: Int, numComp: Int, y: Int, size: Int): Double { + if (calculate) { + this[x + numComp * y] = cos(Math.PI * y * x / size) } + return this[x + numComp * y] } private fun linearToSrgb(value: Float): Int { From f1a9041443c3702a76a0ad57cdaa9766738f6e42 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Tue, 23 Jun 2020 13:44:59 +0200 Subject: [PATCH 10/13] Benchmark output showing coroutines improves performance for images with size >= 40x24 --- .../java/com/wolt/blurhashapp/MainActivity.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index 1d061a2a..0780ba97 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -69,22 +69,22 @@ class Vm : ViewModel() { executor.execute { notifyBenchmark("-----------------------------------") } - for (tasks in 1..6) { + for (tasks in 1..3) { executor.execute { notifyBenchmark("") notifyBenchmark("-----------------------------------") notifyBenchmark("Parallel tasks: $tasks") notifyBenchmark("-----------------------------------") } - for (size in 1..1) { + for (size in 1..3) { val width = 20 * 2.0.pow(size - 1).toInt() val height = 12 * 2.0.pow(size - 1).toInt() executor.execute { notifyBenchmark("width: $width, height: $height") } - for (n in 0..3) { + for (imageCount in 0..2) { executor.execute { - benchmark(10.0.pow(n).toInt(), width, height, blurHash, useCache = true, tasks = tasks) + benchmark(10.0.pow(imageCount).toInt(), width, height, blurHash, useCache = true, tasks = tasks) } } executor.execute { @@ -105,12 +105,13 @@ class Vm : ViewModel() { notifyBenchmark("-> $max bitmaps") var bmp: Bitmap? = null BlurHashDecoder.clearCache() - val time = timed { - for (i in 1..max) { + val listOfTimes = ArrayList() + for (i in 1..max) { + listOfTimes.add(timed { bmp = BlurHashDecoder.decode(blurHash, width, height, useCache = useCache, parallelTasks = tasks) - } + }) } - notifyBenchmark("<- $time ms, Avg: ${time / max.toDouble()} ms") + notifyBenchmark("<- ${listOfTimes.sum()} ms, Avg: ${listOfTimes.sum() / max.toDouble()} ms, Max: ${listOfTimes.max()}, Min: ${listOfTimes.min()}") // log the bitmap size println("bmp size: ${bmp?.byteCount}") } From 05a55398ab38a98860e2136f23dc21a4b1a9b170 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Tue, 23 Jun 2020 14:40:55 +0200 Subject: [PATCH 11/13] using nanoseconds in benchmark --- Kotlin/demo/build.gradle | 2 +- .../demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Kotlin/demo/build.gradle b/Kotlin/demo/build.gradle index f4355af1..4d858939 100644 --- a/Kotlin/demo/build.gradle +++ b/Kotlin/demo/build.gradle @@ -8,7 +8,7 @@ android { defaultConfig { applicationId "com.wolt.blurhash" - minSdkVersion 14 + minSdkVersion 17 targetSdkVersion 29 versionCode 1 versionName "1.0" diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index 0780ba97..a3c08cd8 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -43,9 +43,9 @@ class MainActivity : AppCompatActivity() { * Executes a function and return the time spent in milliseconds. */ private inline fun timed(function: () -> Unit): Long { - val start = SystemClock.elapsedRealtime() + val start = SystemClock.elapsedRealtimeNanos() function() - return SystemClock.elapsedRealtime() - start + return SystemClock.elapsedRealtimeNanos() - start } class Vm : ViewModel() { @@ -111,7 +111,7 @@ class Vm : ViewModel() { bmp = BlurHashDecoder.decode(blurHash, width, height, useCache = useCache, parallelTasks = tasks) }) } - notifyBenchmark("<- ${listOfTimes.sum()} ms, Avg: ${listOfTimes.sum() / max.toDouble()} ms, Max: ${listOfTimes.max()}, Min: ${listOfTimes.min()}") + notifyBenchmark("<- ${listOfTimes.sum() / 1000000.0} ms, Avg: ${listOfTimes.sum() / 1000000.0 / max.toDouble()} ms, Max: ${(listOfTimes.max()?.toDouble() ?: 0.0) / 1000000.0}, Min: ${(listOfTimes.min()?.toDouble() ?: 0.0) / 1000000.0}") // log the bitmap size println("bmp size: ${bmp?.byteCount}") } From 5e63ceb8410d66e5d6ee0654bb3d67a80fc951a9 Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Tue, 23 Jun 2020 15:03:13 +0200 Subject: [PATCH 12/13] code cleanup --- .../src/main/java/com/wolt/blurhashapp/MainActivity.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index a3c08cd8..65d19118 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -48,6 +48,8 @@ private inline fun timed(function: () -> Unit): Long { return SystemClock.elapsedRealtimeNanos() - start } +private const val NANOS = 1000000.0 + class Vm : ViewModel() { private val liveData = MutableLiveData() private val executor = Executors.newSingleThreadExecutor() @@ -111,7 +113,10 @@ class Vm : ViewModel() { bmp = BlurHashDecoder.decode(blurHash, width, height, useCache = useCache, parallelTasks = tasks) }) } - notifyBenchmark("<- ${listOfTimes.sum() / 1000000.0} ms, Avg: ${listOfTimes.sum() / 1000000.0 / max.toDouble()} ms, Max: ${(listOfTimes.max()?.toDouble() ?: 0.0) / 1000000.0}, Min: ${(listOfTimes.min()?.toDouble() ?: 0.0) / 1000000.0}") + notifyBenchmark("<- ${listOfTimes.sum().millis().format()} ms, " + + "Avg: ${(listOfTimes.sum().millis() / max.toDouble()).format()} ms, " + + "Max: ${listOfTimes.max().millis().format()}, " + + "Min: ${listOfTimes.min().millis().format()}") // log the bitmap size println("bmp size: ${bmp?.byteCount}") } @@ -122,3 +127,6 @@ class Vm : ViewModel() { } } } + +private fun Long?.millis() = (this?.toDouble() ?: 0.0) / NANOS +private fun Double.format() = "%.${2}f".format(this) From c95e4c4a4b201365915852553d5b5573fd30a9ea Mon Sep 17 00:00:00 2001 From: Ignacio Tomas Crespo Date: Tue, 23 Jun 2020 16:27:02 +0200 Subject: [PATCH 13/13] bugfix, added UTs, enabled release minify --- Kotlin/demo/build.gradle | 5 +- .../wolt/blurhashkt/BlurHashDecoderTest.kt | 112 ++++++++++++++++++ .../com/wolt/blurhashkt/BlurHashDecoder.kt | 7 +- 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 Kotlin/lib/src/androidTestDebug/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt diff --git a/Kotlin/demo/build.gradle b/Kotlin/demo/build.gradle index 4d858939..48059d83 100644 --- a/Kotlin/demo/build.gradle +++ b/Kotlin/demo/build.gradle @@ -16,8 +16,9 @@ android { buildTypes { release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.debug } } diff --git a/Kotlin/lib/src/androidTestDebug/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt b/Kotlin/lib/src/androidTestDebug/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt new file mode 100644 index 00000000..e44fb372 --- /dev/null +++ b/Kotlin/lib/src/androidTestDebug/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt @@ -0,0 +1,112 @@ +package com.wolt.blurhashkt + +import android.graphics.Bitmap +import com.wolt.blurhashkt.BlurHashDecoder.clearCache +import com.wolt.blurhashkt.BlurHashDecoder.decode +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.nio.ByteBuffer +import java.util.* + + +class BlurHashDecoderTest { + @Before + @Throws(Exception::class) + fun setUp() { + clearCache() + } + + @Test + fun decode_smallImage_cacheEnabled_shouldGetTheSameBitmapInManyRequests() { + val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! + val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! + val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! + val bmp4 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, parallelTasks = 2)!! + val bmp5 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, parallelTasks = 3)!! + + bmp1.assertEquals(bmp2) + bmp2.assertEquals(bmp3) + bmp3.assertEquals(bmp4) + bmp4.assertEquals(bmp5) + } + + @Test + fun decode_smallImage_differentCache_shouldGetTheSameBitmapInManyRequests() { + val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! + val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! + val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! + val bmp4 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false, parallelTasks = 2)!! + val bmp5 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false, parallelTasks = 3)!! + + bmp1.assertEquals(bmp2) + bmp2.assertEquals(bmp3) + bmp3.assertEquals(bmp4) + bmp4.assertEquals(bmp5) + } + + @Test + fun decode_smallImage_cacheDisabled_shouldGetTheSameBitmapInManyRequests() { + val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! + val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! + val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! + val bmp4 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false, parallelTasks = 2)!! + val bmp5 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false, parallelTasks = 3)!! + + bmp1.assertEquals(bmp2) + bmp2.assertEquals(bmp3) + bmp3.assertEquals(bmp4) + bmp4.assertEquals(bmp5) + } + + @Test + fun decode_bigImage_cacheEnabled_shouldGetTheSameBitmapInManyRequests() { + val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! + val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! + val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! + val bmp4 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, parallelTasks = 2)!! + val bmp5 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, parallelTasks = 3)!! + + bmp1.assertEquals(bmp2) + bmp2.assertEquals(bmp3) + bmp3.assertEquals(bmp4) + bmp4.assertEquals(bmp5) + } + + @Test + fun decode_bigImage_differentCache_shouldGetTheSameBitmapInManyRequests() { + val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! + val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! + val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! + val bmp4 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false, parallelTasks = 2)!! + val bmp5 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false, parallelTasks = 3)!! + + bmp1.assertEquals(bmp2) + bmp2.assertEquals(bmp3) + bmp3.assertEquals(bmp4) + bmp4.assertEquals(bmp5) + } + + @Test + fun decode_bigImage_cacheDisabled_shouldGetTheSameBitmapInManyRequests() { + val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! + val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! + val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! + val bmp4 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false, parallelTasks = 2)!! + val bmp5 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false, parallelTasks = 3)!! + + bmp1.assertEquals(bmp2) + bmp2.assertEquals(bmp3) + bmp3.assertEquals(bmp4) + bmp4.assertEquals(bmp5) + } +} + +fun Bitmap.assertEquals(bitmap2: Bitmap) { + val buffer1: ByteBuffer = ByteBuffer.allocate(height * rowBytes) + copyPixelsToBuffer(buffer1) + val buffer2: ByteBuffer = ByteBuffer.allocate(bitmap2.height * bitmap2.rowBytes) + bitmap2.copyPixelsToBuffer(buffer2) + val equals = Arrays.equals(buffer1.array(), buffer2.array()) + assertTrue(equals) +} diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt index 653cb05b..0795cb7a 100644 --- a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ b/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -19,7 +19,7 @@ object BlurHashDecoder { cacheCosinesY.clear() } - fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true, parallelTasks: Int = 3): Bitmap? { + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true, parallelTasks: Int = 1): Bitmap? { if (blurHash == null || blurHash.length < 6) { return null @@ -138,9 +138,12 @@ object BlurHashDecoder { runBlocking { COROUTINES_SCOPE_FOR_PARALLEL_TASKS.launch { val tasks = ArrayList>() - val step = height / parallelTasks + var step = height / parallelTasks for (t in 0 until parallelTasks) { val start = step * t + if (t == parallelTasks - 1 && step * parallelTasks < height) { + step += (height - step * parallelTasks) + } tasks.add(async { for (y in start until start + step) { compositBitmapOnlyX(width, numCompY, numCompX, calculateCosX, cosinesX, calculateCosY, cosinesY, y, height, colors, imageArray)