Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
23 changes: 14 additions & 9 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -109,7 +114,7 @@ fun String.urlEncodePath(): String {
return encoded.toString()
}

fun process_ignore_file(curr_dir: File): Set<String> {
fun process_ignore_file(curr_dir: File, dir_files: List<File>): Set<String> {

val ignore_filename = ".html4ignore"

Expand Down Expand Up @@ -138,7 +143,7 @@ fun process_ignore_file(curr_dir: File): Set<String> {
}
}

curr_dir.list()?.sorted()?.forEach {
dir_files.map { it.name }.sorted().forEach {
val current = it
ignored_regexes.forEach { regex ->
if(regex.matches(current)){
Expand Down Expand Up @@ -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<File>){

val exclude: Set<String> = process_ignore_file(curr_dir)
val exclude: Set<String> = process_ignore_file(curr_dir, dir_files)

val css = """
<style>
Expand Down Expand Up @@ -234,9 +239,9 @@ fun process_dir(curr_dir: File){
val index_middle = fun():String{
val l = StringBuilder()

val dir_files: MutableList<File> = curr_dir.listFiles()?.toMutableList() ?: mutableListOf()
dir_files.sortWith(compareBy ({it.name}) )
dir_files.forEach {
val sorted_files = dir_files.toMutableList()
sorted_files.sortWith(compareBy ({it.name}) )
sorted_files.forEach {
val fileName = it.getName()
// ⚡ Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls
if (fileName !in exclude) {
Expand Down
40 changes: 26 additions & 14 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class MainTest {
File(tempDir, "test.log").createNewFile()
File(tempDir, "test.md").createNewFile()

val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())

assertTrue(excluded.contains("test.txt"))
assertTrue(excluded.contains("test.log"))
Expand All @@ -115,7 +115,7 @@ class MainTest {

@Test
fun testProcessIgnoreFileNoIgnore() {
val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
assertTrue(excluded.contains("index.html"))
assertEquals(9, excluded.size) // index.html + 8 default sensitive files
}
Expand All @@ -128,7 +128,7 @@ class MainTest {
File(tempDir, "test.log").createNewFile()
File(tempDir, "test.txt").createNewFile()

val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())

assertTrue(excluded.contains("test.log"))
assertFalse(excluded.contains("test.txt"))
Expand All @@ -142,7 +142,7 @@ class MainTest {
File(tempDir, "test.ignore").createNewFile()
File(tempDir, ".html4ignore").writeText(".*\\.ignore")

process_dir(tempDir)
process_dir(tempDir, tempDir.listFiles()?.toList() ?: emptyList())

val indexFile = File(tempDir, "index.html")
assertTrue(indexFile.exists())
Expand Down Expand Up @@ -194,7 +194,7 @@ class MainTest {
Assume.assumeTrue("Symlink creation not supported in this environment", false)
}

process_dir(tempDir)
process_dir(tempDir, tempDir.listFiles()?.toList() ?: emptyList())

assertEquals("original content", targetFile.readText())
assertTrue(indexFile.exists())
Expand Down Expand Up @@ -304,14 +304,26 @@ class MainTest {
val ignoreFile = File(tempDir, ".html4ignore")
ignoreFile.writeText("index\\.html")
File(tempDir, "index.html").writeText("existing")
val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
assertTrue(excluded.contains("index.html"))
}

@Test
fun testProcessDirItEqualsCurrDir() {
File(tempDir, "tempDir").mkdir()
process_dir(tempDir)
process_dir(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
}

@Test
fun testProcessDirDefaultArg() {
// Test that the default argument is evaluated properly and covered by tests.
process_dir(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
}

@Test
fun testProcessIgnoreFileDefaultArg() {
// Test that the default argument is evaluated properly and covered by tests.
process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
}

@Test(expected = IllegalArgumentException::class)
Expand Down Expand Up @@ -344,7 +356,7 @@ class MainTest {

File(tempDir, "test.txt").createNewFile()

val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
assertTrue(excluded.contains("test.txt"))
}

Expand All @@ -354,7 +366,7 @@ class MainTest {
ignoreDir.mkdir()

// This should not crash or parse the directory
val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
assertTrue(excluded.contains("index.html"))
}

Expand All @@ -376,7 +388,7 @@ class MainTest {
File(tempDir, "pattern1005").createNewFile() // Should not be ignored as we stop at 1000
File(tempDir, longPattern).createNewFile() // Should not be ignored as length > 100

val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())

assertTrue(excluded.contains("pattern500"))
assertFalse(excluded.contains("pattern1005"))
Expand All @@ -398,7 +410,7 @@ class MainTest {
File(tempDir, "test.txt").createNewFile()

// Should ignore the symlink and NOT parse it
val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
assertFalse(excluded.contains("test.txt"))
assertTrue(excluded.contains("index.html"))
}
Expand All @@ -413,7 +425,7 @@ class MainTest {
File(tempDir, "test.txt").createNewFile()

// Should ignore the file because it's too large
val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
assertFalse(excluded.contains("test.txt"))
assertTrue(excluded.contains("index.html"))
}
Expand All @@ -427,7 +439,7 @@ class MainTest {
File(tempDir, "test.log").createNewFile()
File(tempDir, "test.txt").createNewFile()

val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
// .log is excluded because it's valid
assertTrue(excluded.contains("test.log"))
// test.txt is not excluded because long regex was ignored
Expand All @@ -447,7 +459,7 @@ class MainTest {
File(tempDir, "test.txt1000").createNewFile()
File(tempDir, "test.txt1001").createNewFile()

val excluded = process_ignore_file(tempDir)
val excluded = process_ignore_file(tempDir, tempDir.listFiles()?.toList() ?: emptyList())
// Line 1000 should be processed
assertTrue(excluded.contains("test.txt1000"))
// Line 1001 should be ignored due to line limit
Expand Down
Loading