From bb89ad6c73341a2677b42d6c0a735837c739105a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 4 Jul 2026 20:54:27 +0000 Subject: [PATCH] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0:=20`esc?= =?UTF-8?q?apeHtml`=20=ED=95=A8=EC=88=98=EC=9D=98=20=EC=97=AC=EB=9F=AC=20`?= =?UTF-8?q?replace`=20=ED=98=B8=EC=B6=9C=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 - 여러 번의 `.replace()` 호출로 인한 중간 문자열 할당 방지 - 단일 패스 루프와 지연 초기화(lazy-initialization)된 `StringBuilder` 사용 - 테스트 통과 및 JaCoCo 100% 커버리지 확인 --- .jules/bolt.md | 4 ++++ src/main/kotlin/html4tree/main.kt | 31 +++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 83cc604..1e61273 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,7 @@ ## 2024-05-24 - Loop Allocation Hot Paths **Learning:** Rendering directory entries with repeated string concatenation and list-based exclusion lookups creates avoidable allocation and lookup cost in large directories. **Action:** Use `StringBuilder` for entry rendering and a `Set` for excluded file names. + +## 2024-08-01 - Chained String Replace +**Learning:** Using chained `.replace()` calls on a String for frequent operations (like HTML escaping) is inefficient due to multiple intermediate allocations. +**Action:** Use a single-pass loop with a lazily-initialized `StringBuilder` for string manipulation functions called frequently on hot paths to minimize allocations and improve performance. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 2e2809f..b237de5 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -49,12 +49,31 @@ fun go(topDir: String, maxLevel: Int) { } fun String.escapeHtml(): String { - return this.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - .replace("`", "`") + // ⚡ Bolt: Use a single-pass loop with a lazily-initialized StringBuilder + // to avoid intermediate string allocations from chained replace calls. + var sb: java.lang.StringBuilder? = null + for (i in this.indices) { + val c = this[i] + val replacement = when (c) { + '&' -> "&" + '<' -> "<" + '>' -> ">" + '"' -> """ + '\'' -> "'" + '`' -> "`" + else -> null + } + if (replacement != null) { + if (sb == null) { + sb = java.lang.StringBuilder(this.length + 16) + sb.append(this, 0, i) + } + sb.append(replacement) + } else { + sb?.append(c) + } + } + return sb?.toString() ?: this } fun String.urlEncodePath(): String {