diff --git a/.jules/bolt.md b/.jules/bolt.md index 6904de2..81ed0ea 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -9,3 +9,6 @@ ## 2024-07-26 - Intermediate String Allocations **Learning:** Chained `.replace()` calls on strings in Kotlin (e.g. for HTML escaping) allocate an intermediate String at each step, significantly impacting performance and garbage collection on hot paths with many elements. **Action:** Replace chained `.replace()` calls with a single-pass loop that iterates over characters once, lazily initializing a `StringBuilder` to append the transformed output. +## 2024-05-24 - Optimize URL encoding performance in Kotlin +**Learning:** Chained string manipulations like `.toString(16).padStart(2, '0').toUpperCase()` inside a tight loop create multiple intermediate string objects, leading to high memory allocation and slow execution. Also, using `.forEach` on a ByteArray creates an iterator object overhead compared to a direct loop using `indices`. +**Action:** Replace complex string manipulations for hex encoding with a pre-calculated `HEX_ARRAY` lookup and bitwise operators (`ushr 4` and `and 0x0f`), use `bytes.indices` for loops over primitive arrays to avoid iterator allocation, and pre-size StringBuilders based on expected output length (`bytes.size * 2`) to minimize resizing overhead. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 1710d99..21d81a7 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -80,22 +80,29 @@ fun String.escapeHtml(): String { return sb?.toString() ?: this } +// ⚡ Bolt Performance Optimization: Use pre-calculated hex array and indices loop +// Reduces intermediate string allocations from .toString(16).padStart(2, '0').toUpperCase() +// and avoids iterator creation by using a direct index loop. +internal val HEX_ARRAY = "0123456789ABCDEF".toCharArray() + fun String.urlEncodePath(): String { - val encoded = StringBuilder() - this.toByteArray(Charsets.UTF_8).forEach { - val byte = it.toInt() and 0xff - val isUnreserved = (byte in 'A'.toInt()..'Z'.toInt()) || - (byte in 'a'.toInt()..'z'.toInt()) || - (byte in '0'.toInt()..'9'.toInt()) || - byte == '-'.toInt() || - byte == '.'.toInt() || - byte == '_'.toInt() || - byte == '~'.toInt() + val bytes = this.toByteArray(Charsets.UTF_8) + val encoded = java.lang.StringBuilder(bytes.size * 2) + for (i in bytes.indices) { + val byte = bytes[i].toInt() and 0xff + val isUnreserved = (byte in 65..90) || // 'A'..'Z' + (byte in 97..122) || // 'a'..'z' + (byte in 48..57) || // '0'..'9' + byte == 45 || // '-' + byte == 46 || // '.' + byte == 95 || // '_' + byte == 126 // '~' if (isUnreserved) { encoded.append(byte.toChar()) } else { encoded.append('%') - encoded.append(byte.toString(16).padStart(2, '0').toUpperCase()) + encoded.append(HEX_ARRAY[byte ushr 4]) + encoded.append(HEX_ARRAY[byte and 0x0f]) } } return encoded.toString() diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index a3f8c76..6078b8e 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -310,4 +310,10 @@ class MainTest { assertTrue(excluded.contains("test.txt")) } + + @Test + fun testGetHexArray() { + assertEquals('0', HEX_ARRAY[0]) + assertEquals('F', HEX_ARRAY[15]) + } }