diff --git a/.jules/bolt.md b/.jules/bolt.md index 6904de2..49ce467 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -9,3 +9,7 @@ ## 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-07-31 - GC Overhead from String Padding in Loops +**Learning:** Using `byte.toString(16).padStart(2, '0').toUpperCase()` for hex encoding inside a loop generates significant GC overhead due to multiple intermediate string allocations per byte. +**Action:** Use bitwise shifts (`ushr` and `and`) with a pre-allocated array of hex characters (e.g. `val hexChars = "0123456789ABCDEF"`) to map bytes directly to hex characters, and combine it with a lazy `StringBuilder` to minimize allocations. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 1710d99..46deee4 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -80,10 +80,14 @@ fun String.escapeHtml(): String { return sb?.toString() ?: this } +// ⚡ Bolt Performance Optimization: Reduce GC overhead in urlEncodePath fun String.urlEncodePath(): String { - val encoded = StringBuilder() - this.toByteArray(Charsets.UTF_8).forEach { - val byte = it.toInt() and 0xff + var encoded: StringBuilder? = null + val bytes = this.toByteArray(Charsets.UTF_8) + val hexChars = "0123456789ABCDEF" + + for (i in bytes.indices) { + val byte = bytes[i].toInt() and 0xff val isUnreserved = (byte in 'A'.toInt()..'Z'.toInt()) || (byte in 'a'.toInt()..'z'.toInt()) || (byte in '0'.toInt()..'9'.toInt()) || @@ -92,13 +96,20 @@ fun String.urlEncodePath(): String { byte == '_'.toInt() || byte == '~'.toInt() if (isUnreserved) { - encoded.append(byte.toChar()) + encoded?.append(byte.toChar()) } else { + if (encoded == null) { + encoded = StringBuilder(bytes.size + 16) + for (j in 0 until i) { + encoded.append(bytes[j].toChar()) + } + } encoded.append('%') - encoded.append(byte.toString(16).padStart(2, '0').toUpperCase()) + encoded.append(hexChars[byte ushr 4]) + encoded.append(hexChars[byte and 0x0F]) } } - return encoded.toString() + return encoded?.toString() ?: this } fun process_ignore_file(curr_dir: File): Set {