diff --git a/.jules/bolt.md b/.jules/bolt.md index 82fe47b..5cf5ec7 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -13,3 +13,6 @@ **Learning:** `byte.toString(16).padStart(2, '0').toUpperCase()` inside a loop allocating up to 3 strings per reserved byte in a hot path causes significant GC pressure. This is a common but dangerous anti-pattern in Kotlin when processing large strings or numerous files in directory crawlers. **Action:** Replace chained string operations with direct character mapping and bitwise operations (`ushr`, `and`) when building formatted hex output, which avoids intermediate string creation entirely. Ensure 100% branch coverage with test inputs spanning both < 10 and > 9 hex values. ## 2026-07-10 - Expensive OS stat calls before cheap in-memory checks\n**Learning:** In Kotlin/Java, checking file properties (like `isDirectory` or `isSymbolicLink`) via `java.nio.file.Files` requires allocating `Path` objects and performs expensive native OS stat calls. When processing file listings, always short-circuit filesystem checks by testing against exclusion lists (using cheap in-memory string operations) before calling methods that touch the filesystem.\n**Action:** Re-ordered conditionals to check `exclude` sets before invoking `Files.isDirectory` and `Files.isSymbolicLink`. +## 2024-07-11 - [Eliminating Redundant Directory Listings] +**Learning:** In nested file processing functions like `process_dir` and `process_ignore_file`, executing `curr_dir.listFiles()` repeatedly for the same directory is highly inefficient and creates expensive duplicate OS syscalls. +**Action:** Always hoist I/O intensive listing operations like `listFiles()` to the highest appropriate scope and pass the result as a parameter to child functions rather than re-evaluating it. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 936c2b7..3141b09 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -40,11 +40,16 @@ fun go(topDir: String, maxLevel: Int) { while(lle != null && Files.isDirectory(lle.file.toPath(), LinkOption.NOFOLLOW_LINKS)){ val currentLevel: Int = lle.level + // ⚡ Bolt Performance Optimization: Hoist listFiles() out of nested functions + // By fetching directory contents once, we eliminate redundant OS syscalls + // in process_dir and process_ignore_file (saves 2x I/O calls per directory). + val dir_files = lle.file.listFiles()?.toList() ?: emptyList() + if(maxLevel == -1 || currentLevel <= maxLevel) - process_dir(lle.file) + process_dir(lle.file, dir_files) if(maxLevel == -1 || currentLevel < maxLevel) { - lle.file.listFiles()?.forEach { + dir_files.forEach { if(Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS)){ ll.push( LinkedListEntry(it, currentLevel+1)) } @@ -109,7 +114,7 @@ fun String.urlEncodePath(): String { return encoded.toString() } -fun process_ignore_file(curr_dir: File): Set { +fun process_ignore_file(curr_dir: File, dir_files: List): Set { val ignore_filename = ".html4ignore" @@ -138,7 +143,7 @@ fun process_ignore_file(curr_dir: File): Set { } } - curr_dir.list()?.sorted()?.forEach { + dir_files.map { it.name }.sorted().forEach { val current = it ignored_regexes.forEach { regex -> if(regex.matches(current)){ @@ -169,9 +174,9 @@ fun write_index_file(curr_dir: File, content: String) { } } -fun process_dir(curr_dir: File){ +fun process_dir(curr_dir: File, dir_files: List){ - val exclude: Set = process_ignore_file(curr_dir) + val exclude: Set = process_ignore_file(curr_dir, dir_files) val css = """