diff --git a/.jules/bolt.md b/.jules/bolt.md index 3ebd2a5..67f16f5 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -30,3 +30,7 @@ ## 2024-05-18 - [디렉토리 목록 캐싱을 통한 I/O 오버헤드 최적화] **Learning:** `process_dir` 및 `process_ignore_file`과 같은 함수에서 동일한 디렉토리에 대해 `listFiles()` 또는 `list()`를 반복적으로 호출하면, 파일 시스템 I/O로 인한 불필요한 성능 저하가 발생합니다. **Action:** 디렉토리를 순회할 때 상위 루프에서 `listFiles()`를 한 번만 호출하여 캐싱한 후, 결과를 인자로 전달(예: `dirFiles` 배열)하여 중복된 파일 시스템 호출을 제거해야 합니다. + +## 2024-08-01 - URL 인코딩 빌더 지연 생성 +**학습:** URL 인코딩이 필요 없는 안전한 경로 문자열에서도 항상 `StringBuilder`를 생성하면 hot path에서 불필요한 할당이 발생합니다. +**조치:** 예약 바이트를 처음 만났을 때만 `StringBuilder`를 만들고, 그 전까지는 원본 문자열을 그대로 반환하는 지연 생성 패턴을 사용합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 2698867..2df4131 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -138,9 +138,10 @@ fun String.escapeHtml(): String { } fun String.urlEncodePath(): String { - val encoded = StringBuilder() - this.toByteArray(Charsets.UTF_8).forEach { - val byte = it.toInt() and 0xff + val bytes = this.toByteArray(Charsets.UTF_8) + var encoded: StringBuilder? = null + 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()) || @@ -149,18 +150,26 @@ fun String.urlEncodePath(): String { byte == '_'.toInt() || byte == '~'.toInt() if (isUnreserved) { - encoded.append(byte.toChar()) + encoded?.append(byte.toChar()) } else { + var builder = encoded + if (builder == null) { + builder = StringBuilder(bytes.size + 16) + for (j in 0 until i) { + builder.append((bytes[j].toInt() and 0xff).toChar()) + } + encoded = builder + } // ⚡ Bolt Performance Optimization: Direct character mapping // Avoids multiple string allocations (toString, padStart, toUpperCase) per reserved byte. - encoded.append('%') + builder.append('%') val hex1 = byte ushr 4 val hex2 = byte and 0xf - encoded.append(if (hex1 < 10) (hex1 + 48).toChar() else (hex1 + 55).toChar()) - encoded.append(if (hex2 < 10) (hex2 + 48).toChar() else (hex2 + 55).toChar()) + builder.append(if (hex1 < 10) (hex1 + 48).toChar() else (hex1 + 55).toChar()) + builder.append(if (hex2 < 10) (hex2 + 48).toChar() else (hex2 + 55).toChar()) } } - return encoded.toString() + return encoded?.toString() ?: this } fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): Set {