Skip to content

Commit a8040d3

Browse files
authored
Merge pull request #1614 from square/jwilson.0409.promote_fast_js_hex
Custom JS implementation of decodeHex
2 parents 9b42a97 + a571c31 commit a8040d3

File tree

4 files changed

+125
-21
lines changed

4 files changed

+125
-21
lines changed

okio/build.gradle.kts

+9-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ plugins {
4444
* '-- wasmWasi
4545
* ```
4646
*
47-
* The `nonJvm` source set excludes that platform.
47+
* The `nonJvm`, `nonJs`, `nonApple`, etc. source sets exclude the corresponding platforms.
4848
*
4949
* The `hashFunctions` source set builds on all platforms. It ships as a main source set on non-JVM
5050
* platforms and as a test source set on the JVM platform.
@@ -93,6 +93,10 @@ kotlin {
9393
dependsOn(commonMain)
9494
}
9595

96+
val nonJsMain by creating {
97+
dependsOn(commonMain)
98+
}
99+
96100
val systemFileSystemMain by creating {
97101
dependsOn(commonMain)
98102
}
@@ -115,6 +119,7 @@ kotlin {
115119
val jvmMain by getting {
116120
dependsOn(zlibMain)
117121
dependsOn(systemFileSystemMain)
122+
dependsOn(nonJsMain)
118123
}
119124
val jvmTest by getting {
120125
kotlin.srcDir("src/hashFunctions")
@@ -149,9 +154,11 @@ kotlin {
149154
children = mingwTargets,
150155
).also { mingwMain ->
151156
mingwMain.dependsOn(nonAppleMain)
157+
mingwMain.dependsOn(nonJsMain)
152158
}
153159
createSourceSet("unixMain", parent = nativeMain)
154160
.also { unixMain ->
161+
unixMain.dependsOn(nonJsMain)
155162
createSourceSet(
156163
"linuxMain",
157164
parent = unixMain,
@@ -175,6 +182,7 @@ kotlin {
175182
if (kmpWasmEnabled) {
176183
createSourceSet("wasmMain", parent = commonMain, children = wasmTargets)
177184
.also { wasmMain ->
185+
wasmMain.dependsOn(nonJsMain)
178186
wasmMain.dependsOn(nonJvmMain)
179187
wasmMain.dependsOn(nonAppleMain)
180188
}

okio/src/commonMain/kotlin/okio/internal/ByteString.kt

+1-20
Original file line numberDiff line numberDiff line change
@@ -289,32 +289,13 @@ internal inline fun String.commonDecodeBase64(): ByteString? {
289289
}
290290

291291
@Suppress("NOTHING_TO_INLINE")
292-
internal inline fun String.commonDecodeHex(): ByteString {
293-
require(length % 2 == 0) { "Unexpected hex string: $this" }
294-
295-
val result = ByteArray(length / 2)
296-
for (i in result.indices) {
297-
val d1 = decodeHexDigit(this[i * 2]) shl 4
298-
val d2 = decodeHexDigit(this[i * 2 + 1])
299-
result[i] = (d1 + d2).toByte()
300-
}
301-
return ByteString(result)
302-
}
292+
internal expect inline fun String.commonDecodeHex(): ByteString
303293

304294
/** Writes the contents of this byte string to `buffer`. */
305295
internal fun ByteString.commonWrite(buffer: Buffer, offset: Int, byteCount: Int) {
306296
buffer.write(data, offset, byteCount)
307297
}
308298

309-
private fun decodeHexDigit(c: Char): Int {
310-
return when (c) {
311-
in '0'..'9' -> c - '0'
312-
in 'a'..'f' -> c - 'a' + 10
313-
in 'A'..'F' -> c - 'A' + 10
314-
else -> throw IllegalArgumentException("Unexpected hex digit: $c")
315-
}
316-
}
317-
318299
@Suppress("NOTHING_TO_INLINE")
319300
internal inline fun ByteString.commonToString(): String {
320301
if (data.isEmpty()) return "[size=0]"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (C) 2025 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package okio.internal
17+
18+
import okio.ByteString
19+
20+
private val charToNibble = js(
21+
"""
22+
{
23+
0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
24+
a: 10, b: 11, c: 12, d: 13, e: 14, f: 15,
25+
A: 10, B: 11, C: 12, D: 13, E: 14, F: 15
26+
}
27+
""",
28+
)
29+
30+
/**
31+
* Here we implement a custom hex decoder because the vanilla Kotlin one is too slow. The Kotlin
32+
* transpiles to reasonable-looking but very inefficient JavaScript!
33+
*
34+
* This does a plain JavaScript implementation of hex decoding, and it's dramatically faster. In
35+
* one measurement hex decoding went from 25% of CPU samples to 0% of them.
36+
*/
37+
@Suppress("NOTHING_TO_INLINE")
38+
internal actual inline fun String.commonDecodeHex(): ByteString {
39+
require(length % 2 == 0) { "Unexpected hex string: $this" }
40+
41+
val string = this
42+
val charToNibble = charToNibble
43+
val result = ByteArray(string.length / 2)
44+
var invalidDigitIndex = -1
45+
46+
js(
47+
"""
48+
var stringIndex = 0;
49+
var byteIndex = 0;
50+
while (stringIndex < string.length) {
51+
var charA = string[stringIndex++];
52+
var nibbleA = charToNibble[charA];
53+
54+
var charB = string[stringIndex++];
55+
var nibbleB = charToNibble[charB];
56+
57+
if (nibbleA == null || nibbleB == null) {
58+
invalidDigitIndex = stringIndex;
59+
break;
60+
}
61+
62+
result[byteIndex++] = (nibbleA << 4) | nibbleB;
63+
}
64+
""",
65+
)
66+
67+
require(invalidDigitIndex == -1) {
68+
"Unexpected hex digit: ${string[invalidDigitIndex]}"
69+
}
70+
71+
return ByteString(result)
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (C) 2025 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:JvmName("-ByteStringNonJs") // A leading '-' hides this class from Java.
17+
18+
package okio.internal
19+
20+
import kotlin.jvm.JvmName
21+
import okio.ByteString
22+
23+
@Suppress("NOTHING_TO_INLINE")
24+
internal actual inline fun String.commonDecodeHex(): ByteString {
25+
require(length % 2 == 0) { "Unexpected hex string: $this" }
26+
27+
val result = ByteArray(length / 2)
28+
for (i in result.indices) {
29+
val d1 = decodeHexDigit(this[i * 2]) shl 4
30+
val d2 = decodeHexDigit(this[i * 2 + 1])
31+
result[i] = (d1 + d2).toByte()
32+
}
33+
return ByteString(result)
34+
}
35+
36+
private fun decodeHexDigit(c: Char): Int {
37+
return when (c) {
38+
in '0'..'9' -> c - '0'
39+
in 'a'..'f' -> c - 'a' + 10
40+
in 'A'..'F' -> c - 'A' + 10
41+
else -> throw IllegalArgumentException("Unexpected hex digit: $c")
42+
}
43+
}

0 commit comments

Comments
 (0)