diff --git a/.jules/bolt.md b/.jules/bolt.md index 83cc604..f48246a 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-07-02 - Chained String Replaces +**Learning:** Using multiple chained `.replace()` calls on a Kotlin String (e.g., for HTML escaping) allocates new intermediate strings and arrays for every replacement pass, causing significant GC pressure and CPU overhead when called frequently on long lists. +**Action:** Replace chained `.replace()` calls with a single-pass loop over the string characters. Use a lazily-initialized `StringBuilder` to append escaped values and original characters, avoiding allocations completely on the fast path (when no escaping is needed). diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 2e2809f..35f51a3 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -23,7 +23,7 @@ fun main(args: Array) = Html4tree().main(args) fun go(topDir: String, maxLevel: Int) { require(topDir.isNotBlank()) - val top_dir = File(topDir).canonicalFile + val top_dir = File(topDir).toPath().toAbsolutePath().normalize().toFile() require(Files.isDirectory(top_dir.toPath(), LinkOption.NOFOLLOW_LINKS)) { "Top directory must be an existing non-symlink directory" } val ll = LinkedList() @@ -49,12 +49,29 @@ fun go(topDir: String, maxLevel: Int) { } fun String.escapeHtml(): String { - return this.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - .replace("`", "`") + var encoded: StringBuilder? = null + for (i in 0 until this.length) { + val c = this[i] + val replacement = when (c) { + '&' -> "&" + '<' -> "<" + '>' -> ">" + '"' -> """ + '\'' -> "'" + '`' -> "`" + else -> null + } + if (replacement != null) { + if (encoded == null) { + encoded = StringBuilder(this.length + 16) + encoded.append(this, 0, i) + } + encoded.append(replacement) + } else { + encoded?.append(c) + } + } + return encoded?.toString() ?: this } fun String.urlEncodePath(): String {