From 27755543d683c7f75e0f07b7b395e6d1d8f75ea4 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 7 Jul 2026 03:13:29 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0]=20urlEncodePath=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URL 인코딩 시 반복적인 중간 문자열 생성을 방지하고 비트 연산을 사용하여 StringBuilder 객체의 메모리 할당 및 GC 부하를 최소화했습니다. --- .jules/bolt.md | 4 ++++ src/main/kotlin/html4tree/main.kt | 27 +++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 6904de2..e26de81 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-28 - Number Formatting Object Allocations +**Learning:** Using built-in chained methods like `.toString(16).padStart(2, '0').toUpperCase()` inside a tight loop for URL encoding bytes creates excessive short-lived `String` objects, degrading performance and increasing GC pressure. +**Action:** Replace high-frequency byte-to-hex formatting with direct lookup arrays and bitwise operations (`hex[byte ushr 4]` and `hex[byte and 0x0F]`) to emit characters directly without intermediate objects. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 1710d99..faf2f22 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -80,10 +80,16 @@ fun String.escapeHtml(): String { return sb?.toString() ?: this } +// ⚡ Bolt Performance Optimization: Single-pass loop with lazy StringBuilder and no intermediate strings +// 💡 What: Lazy StringBuilder allocation and direct hex conversion using bitwise operations +// 🎯 Why: Previously, chained string methods (toString.padStart.toUpperCase) created multiple short-lived objects per encoded byte, and StringBuilder was always allocated. +// 📊 Impact: Significantly reduces GC pressure and memory allocations when encoding URLs, especially for ASCII paths which now allocate nothing. +// 🔬 Measurement: Verify with profiler during generation on large directory trees with many special characters. 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) + 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()) || @@ -91,14 +97,23 @@ fun String.urlEncodePath(): String { byte == '.'.toInt() || 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].toInt().toChar()) + } + } encoded.append('%') - encoded.append(byte.toString(16).padStart(2, '0').toUpperCase()) + val hex = "0123456789ABCDEF" + encoded.append(hex[byte ushr 4]) + encoded.append(hex[byte and 0x0F]) } } - return encoded.toString() + return encoded?.toString() ?: this } fun process_ignore_file(curr_dir: File): Set {