Increasing the speed of the Robohash SVG to buffer function

This commit is contained in:
Vitor Pamplona 2024-02-21 13:51:27 -05:00
parent 0429c4dc3a
commit 4b9a55e178
4 changed files with 64 additions and 43 deletions

View File

@ -30,9 +30,10 @@ import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.Options import coil.request.Options
import com.vitorpamplona.amethyst.commons.Robohash
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.utils.Robohash import okio.buffer
import okio.Buffer import okio.source
import java.nio.charset.Charset import java.nio.charset.Charset
@Stable @Stable
@ -43,14 +44,10 @@ class HashImageFetcher(
) : Fetcher { ) : Fetcher {
override suspend fun fetch(): SourceResult { override suspend fun fetch(): SourceResult {
checkNotInMainThread() checkNotInMainThread()
val source = val source =
try { try {
val buffer = Buffer() Robohash.assemble(data.toString(), isLightTheme).byteInputStream(Charset.defaultCharset()).source().buffer()
buffer.writeString(
Robohash.assemble(data.toString(), isLightTheme),
Charset.defaultCharset(),
)
buffer
} finally { } finally {
} }

View File

@ -18,16 +18,20 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.quartz.benchmark package com.vitorpamplona.amethyst.benchmark
import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.utils.Robohash import com.vitorpamplona.amethyst.commons.Robohash
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import okio.Buffer
import okio.buffer
import okio.source
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.nio.charset.Charset
/** /**
* Benchmark, which will execute on an Android device. * Benchmark, which will execute on an Android device.
@ -41,7 +45,7 @@ class RobohashBenchmark {
val warmHex = "f4f016c739b8ec0d6313540a8b12cf48a72b485d38338627ec9d427583551f9a" val warmHex = "f4f016c739b8ec0d6313540a8b12cf48a72b485d38338627ec9d427583551f9a"
val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf" val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf"
val resultingSVG = val expectedTestSVG =
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 300 300\">" + "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 300 300\">" +
"<defs>" + "<defs>" +
"<style>" + "<style>" +
@ -196,7 +200,26 @@ class RobohashBenchmark {
Robohash.assemble(warmHex, true) Robohash.assemble(warmHex, true)
benchmarkRule.measureRepeated { benchmarkRule.measureRepeated {
val result = Robohash.assemble(testHex, true) val result = Robohash.assemble(testHex, true)
assertEquals(resultingSVG, result) assertEquals(expectedTestSVG, result)
}
}
@Test
fun createSVGInBufferCopy() {
// warm up
Robohash.assemble(warmHex, true)
benchmarkRule.measureRepeated {
val buffer = Buffer()
buffer.writeString(Robohash.assemble(testHex, true), Charset.defaultCharset())
}
}
@Test
fun createSVGInBufferViaInputStream() {
// warm up
Robohash.assemble(warmHex, true)
benchmarkRule.measureRepeated {
Robohash.assemble(testHex, true).byteInputStream(Charset.defaultCharset()).source().buffer()
} }
} }
} }

View File

@ -35,6 +35,8 @@ android {
} }
dependencies { dependencies {
implementation project(path: ':quartz')
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

View File

@ -18,10 +18,9 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.quartz.utils package com.vitorpamplona.amethyst.commons
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexValidator import com.vitorpamplona.quartz.encoders.HexValidator
@ -55,9 +54,7 @@ object Robohash {
private fun reduce( private fun reduce(
start: Int, start: Int,
channel: Byte, channel: Byte,
): Int { ) = (start + (channel.toUByte().toInt() * 0.3906f)).toInt()
return (start + (channel.toUByte().toInt() * 0.3906f)).toInt()
}
private fun bytesToRGB( private fun bytesToRGB(
r: Byte, r: Byte,
@ -85,6 +82,7 @@ object Robohash {
Log.w("Robohash", "$msg is not a hex") Log.w("Robohash", "$msg is not a hex")
CryptoUtils.sha256(msg.toByteArray()) CryptoUtils.sha256(msg.toByteArray())
} }
val bgColor = bytesToRGB(hash[0], hash[1], hash[2], isLightTheme) val bgColor = bytesToRGB(hash[0], hash[1], hash[2], isLightTheme)
val fgColor = bytesToRGB(hash[3], hash[4], hash[5], !isLightTheme) val fgColor = bytesToRGB(hash[3], hash[4], hash[5], !isLightTheme)
val body = bodies[byteMod10(hash[6])] val body = bodies[byteMod10(hash[6])]
@ -110,41 +108,42 @@ object Robohash {
BACKGROUND.length + BACKGROUND.length +
END.length END.length
val result = StringBuilder(capacity) val result =
buildString(capacity) {
append(HEADER)
result.append(HEADER) append(".cls-bg{fill:")
append(bgColor)
append(";}.cls-fill-1{fill:")
append(fgColor)
append(";}.cls-fill-2{fill:")
append(fgColor)
append(";}")
result.append(".cls-bg{fill:") append(body.style)
result.append(bgColor) append(face.style)
result.append(";}.cls-fill-1{fill:") append(eye.style)
result.append(fgColor) append(mouth.style)
result.append(";}.cls-fill-2{fill:") append(accessory.style)
result.append(fgColor)
result.append(";}")
result.append(body.style) append(MID)
result.append(face.style)
result.append(eye.style)
result.append(mouth.style)
result.append(accessory.style)
result.append(MID) append(BACKGROUND)
append(body.paths)
append(face.paths)
append(eye.paths)
append(mouth.paths)
append(accessory.paths)
result.append(BACKGROUND) append(END)
result.append(body.paths)
result.append(face.paths)
result.append(eye.paths)
result.append(mouth.paths)
result.append(accessory.paths)
result.append(END)
val resultStr = result.toString()
check(resultStr.length == capacity) { "${resultStr.length} was different from $capacity" }
return resultStr
} }
@Immutable private data class Part(val style: String, val paths: String) check(result.length == capacity) { "${result.length} was different from $capacity" }
return result
}
private data class Part(val style: String, val paths: String)
const val HEADER = const val HEADER =
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 300 300\"><defs><style>" "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 300 300\"><defs><style>"