Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2024-06-21 - Regex Compilation in Loops
**Learning:** In Kotlin, compiling regular expressions (`.toRegex()`) inside a loop over files is a significant O(N * M) performance bottleneck when processing ignore files (N files * M rules).
**Action:** Always map string rules to compiled `Regex` objects outside of the file iteration loop (O(M) compilation) to avoid unnecessary regex re-compilations.

## 2024-06-28 - Avoid O(n^2) operations when processing lists
**Learning:** Checking for containment (`in` / `!in`) within a `List` structure scales as O(n). When executed repeatedly inside a loop covering all `n` files of a directory listing, it inherently creates an O(n^2) operation, acting as a performance pitfall as directory sizes grow. Additionally, repeatedly concatenating strings (`+=`) in such loops leads to O(n^2) memory reallocation operations.
**Action:** When performing `n` membership queries against an exclusion list or tracking items, convert the collection to a `Set` for O(1) lookups. In Kotlin, use a `StringBuilder` or `.joinToString` rather than concatenating with `+=` within iterative structures to optimize performance.
19 changes: 11 additions & 8 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,23 @@ fun String.urlEncodePath(): String {
return java.net.URLEncoder.encode(this, "UTF-8").replace("+", "%20")
}

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

val ignore_filename = ".html4ignore"

val ignore_file_path = curr_dir.getAbsolutePath()+"/"+ignore_filename

val ignore_file = File(ignore_file_path)

val files_to_exclude = mutableListOf<String>()
// BOLT OPTIMIZATION: Use mutableSetOf instead of mutableListOf for O(1) containment checks below
val files_to_exclude = mutableSetOf<String>()

if(ignore_file.exists()){
val ignored_regexes = mutableListOf<Regex>()

ignore_file.forEachLine { ignored_regexes.add(("^"+it+"$").toRegex()) }

curr_dir.list().sorted().forEach {
(curr_dir.list() ?: emptyArray()).forEach {
val current = it
ignored_regexes.forEach { regex ->
if(regex.matches(current)){
Expand All @@ -87,7 +88,7 @@ fun process_ignore_file(curr_dir: File): List<String> {

fun process_dir(curr_dir: File){

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

val css = """
<style>
Expand Down Expand Up @@ -124,18 +125,20 @@ fun process_dir(curr_dir: File){
"""

val index_middle = fun():String{
var l=""
// BOLT OPTIMIZATION: Use StringBuilder instead of string concatenation (+=) to avoid O(n^2) allocations
val l = StringBuilder()

val dir_files: MutableList<File> = curr_dir.listFiles()?.toMutableList() ?: mutableListOf()
val dir_files: MutableList<File> = (curr_dir.listFiles() ?: emptyArray()).toMutableList()
dir_files.sortWith(compareBy ({it.name}) )
dir_files.forEach {
val isLinkedDirectory = it.isDirectory() && !java.nio.file.Files.isSymbolicLink(it.toPath())
if((it.getName() !in exclude) && (isLinkedDirectory || !it.isDirectory())) {
l += """ <li><a style="display:block; width:100%" href="${if (isLinkedDirectory) { "./${it.getName().urlEncodePath()}/" } else { "./${it.getName().urlEncodePath()}" }}">${if (isLinkedDirectory) { "&#128193;" } else { "&rtrif;" }} ${it.getName().escapeHtml()}</a></li>"""+"\n"
l.append(""" <li><a style="display:block; width:100%" href="${if (isLinkedDirectory) { "./${it.getName().urlEncodePath()}/" } else { "./${it.getName().urlEncodePath()}" }}">${if (isLinkedDirectory) { "&#128193;" } else { "&rtrif;" }} ${it.getName().escapeHtml()}</a></li>""")
l.append('\n')
}
}

return l;
return l.toString()
}

val index_bottom="""
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/html4tree/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class LinkedList {
if(l == null){
return null
} else {
l.next = null
l.next = null
return LinkedListEntry(l.data, l.level)
}
}
Expand Down
89 changes: 89 additions & 0 deletions src/test/kotlin/html4tree/LinkedListTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package html4tree

import org.junit.Test
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class LinkedListTest {

@Test
fun testLinkedListEntry() {
val file = File("test")
val entry = LinkedListEntry(file, 1)
assertEquals(file, entry.file)
assertEquals(1, entry.level)
val entryCopy = entry.copy()
assertEquals(entry, entryCopy)
assertTrue(entry.hashCode() == entryCopy.hashCode())
assertTrue(entry.toString().contains("LinkedListEntry"))
assertEquals(file, entry.component1())
assertEquals(1, entry.component2())
}

@Test
fun testEntry() {
val file = File("test")
var entry = Entry(file, 1, null)
assertEquals(file, entry.data)
assertEquals(1, entry.level)
assertNull(entry.next)
val entry2 = Entry(file, 2, null)
entry.next = entry2
assertEquals(entry2, entry.next)
val entryCopy = entry.copy()
assertEquals(entry, entryCopy)
assertTrue(entry.hashCode() == entryCopy.hashCode())
assertTrue(entry.toString().contains("Entry"))
assertEquals(file, entry.component1())
assertEquals(1, entry.component2())
assertEquals(entry2, entry.component3())
}

@Test
fun testLinkedListPushPull() {
val list = LinkedList()

assertNull(list.first)
assertNull(list.last)

assertNull(list.pull())

val entry1 = LinkedListEntry(File("test1"), 1)
val entry2 = LinkedListEntry(File("test2"), 2)

list.push(entry1)

assertNotNull(list.first)
assertNotNull(list.last)
assertEquals(File("test1"), list.first?.data)
assertEquals(File("test1"), list.last?.data)

list.push(entry2)

val pulled1 = list.pull()
assertNotNull(pulled1)
assertEquals(File("test1"), pulled1?.file)
assertEquals(1, pulled1?.level)

val pulled2 = list.pull()
assertNotNull(pulled2)
assertEquals(File("test2"), pulled2?.file)
assertEquals(2, pulled2?.level)

assertNull(list.pull())

// Add additional push to make sure branch coverage for first?.next is hit properly
val list2 = LinkedList()
list2.push(LinkedListEntry(File("testA"), 1))
list2.push(LinkedListEntry(File("testB"), 2))
list2.push(LinkedListEntry(File("testC"), 3))

assertEquals(File("testA"), list2.pull()?.file)
assertEquals(File("testB"), list2.pull()?.file)
assertEquals(File("testC"), list2.pull()?.file)
assertNull(list2.pull())
}
}
20 changes: 20 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ class MainTest {
assertEquals(1, excluded.size)
}

@Test
fun testProcessIgnoreFileUnreadableDirectory() {
File(tempDir, ".html4ignore").writeText(".*")
tempDir.setWritable(true)
tempDir.setExecutable(true)

try {
Assume.assumeTrue(tempDir.setReadable(false, false))
assertNull(tempDir.list())

val excluded = process_ignore_file(tempDir)

assertEquals(setOf("index.html"), excluded)
} finally {
tempDir.setReadable(true, false)
tempDir.setWritable(true, false)
tempDir.setExecutable(true, false)
}
}

@Test
fun testProcessDir() {
val subdir = File(tempDir, "subdir")
Expand Down