Skip to content

Commit f64b59e

Browse files
authored
Revert ArrayBuffer to toByteArray conversion change and use linear wasm memory (#5550)
Measurements of copying ArrayBuffer (1kk elements) to ByteArray: ``` kotlinx-browser toByteArray: ~4-5ms fastArrayBufferToByteArray: ~800µs ``` ## Testing N/A ## Release Notes N/A
1 parent 3400294 commit f64b59e

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@ import kotlinx.browser.window
44
import kotlinx.coroutines.await
55
import org.khronos.webgl.ArrayBuffer
66
import org.khronos.webgl.Int8Array
7-
import org.khronos.webgl.toByteArray
7+
import org.w3c.fetch.Response
88
import org.w3c.files.Blob
99
import org.w3c.xhr.XMLHttpRequest
1010
import kotlin.js.Promise
11+
import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
12+
import kotlin.wasm.unsafe.withScopedMemoryAllocator
13+
14+
@JsFun(
15+
""" (src, size, dstAddr) => {
16+
const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size);
17+
mem8.set(src);
18+
}
19+
"""
20+
)
21+
private external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int)
1122

1223
@JsFun("(blob) => blob.arrayBuffer()")
1324
private external fun jsExportBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer>
@@ -48,7 +59,7 @@ internal object DefaultWasmResourceReader : ResourceReader {
4859

4960
private suspend fun Blob.asByteArray(): ByteArray {
5061
val buffer: ArrayBuffer = jsExportBlobAsArrayBuffer(this).await()
51-
return Int8Array(buffer).toByteArray()
62+
return fastArrayBufferToByteArray(buffer)
5263
}
5364
}
5465

@@ -74,11 +85,24 @@ private object TestWasmResourceReader : ResourceReader {
7485
request.overrideMimeType("text/plain; charset=x-user-defined")
7586
request.send()
7687
if (request.status == 200.toShort()) {
77-
return requestResponseAsByteArray(request).toByteArray()
88+
return requestResponseAsByteArray(request).asByteArray()
7889
}
7990
println("Request status is not 200 - $resPath, status: ${request.status}")
8091
throw MissingResourceException(resPath)
8192
}
93+
94+
private fun Int8Array.asByteArray(): ByteArray {
95+
val array = this
96+
val size = array.length
97+
98+
@OptIn(UnsafeWasmMemoryApi::class)
99+
return withScopedMemoryAllocator { allocator ->
100+
val memBuffer = allocator.allocate(size)
101+
val dstAddress = memBuffer.address.toInt()
102+
jsExportInt8ArrayToWasm(array, size, dstAddress)
103+
ByteArray(size) { i -> (memBuffer + i).loadByte() }
104+
}
105+
}
82106
}
83107

84108
// For blocking XmlHttpRequest the response can be only in text form, so we convert it to bytes manually
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.jetbrains.compose.resources
2+
3+
import org.khronos.webgl.ArrayBuffer
4+
import kotlin.wasm.unsafe.Pointer
5+
import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
6+
import kotlin.wasm.unsafe.withScopedMemoryAllocator
7+
8+
@OptIn(UnsafeWasmMemoryApi::class, ExperimentalWasmJsInterop::class)
9+
internal fun fastArrayBufferToByteArray(arrayBuffer: ArrayBuffer): ByteArray {
10+
val size = arrayBuffer.byteLength
11+
return withScopedMemoryAllocator { allocator ->
12+
val bufferPtr = allocator.allocate(size)
13+
copyArrayBufferToWasmMemory(arrayBuffer, bufferPtr.address.toInt())
14+
readFromLinearMemory(bufferPtr, 0, size)
15+
}
16+
}
17+
18+
//language=js
19+
@OptIn(ExperimentalWasmJsInterop::class)
20+
private fun copyArrayBufferToWasmMemory(ab: ArrayBuffer, ptr: Int): Unit = js("""{
21+
const data = new Uint8Array(ab);
22+
new Uint8Array(wasmExports.memory.buffer).set(data, ptr);
23+
}""")
24+
25+
@OptIn(UnsafeWasmMemoryApi::class)
26+
private fun readFromLinearMemory(base: Pointer, offset: Int, length: Int): ByteArray {
27+
val bytes = ByteArray(length)
28+
val src = base + offset
29+
val intCount = length / 4
30+
var idx = 0
31+
for (i in 0 until intCount) {
32+
val value = (src + idx).loadInt()
33+
bytes[idx] = (value and 0xFF).toByte()
34+
bytes[idx + 1] = ((value shr 8) and 0xFF).toByte()
35+
bytes[idx + 2] = ((value shr 16) and 0xFF).toByte()
36+
bytes[idx + 3] = ((value shr 24) and 0xFF).toByte()
37+
idx += 4
38+
}
39+
for (i in idx until length) {
40+
bytes[i] = (src + i).loadByte()
41+
}
42+
return bytes
43+
}

0 commit comments

Comments
 (0)