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
Expand Up @@ -26,3 +26,7 @@
## 2024-07-28 - 디렉토리 목록 불필요한 정렬 오버헤드
**학습:** 디렉토리 목록(`list()` 또는 `listFiles()`)을 단순히 필터링하여 `Set`에 추가하는 경우처럼 특정 순서가 필요하지 않은 작업에서 `.sorted()`를 호출하면 불필요한 O(N log N) 오버헤드가 발생합니다.
**조치:** `Set`과 같은 순서에 무관한 자료구조에 요소를 추가하기 위한 필터링 작업에서는 디렉토리 목록에서 `.sorted()` 호출을 제거하여 성능을 최적화합니다.

## 2026-07-13 - 디렉토리 스냅샷 재사용
**학습:** 동일 디렉토리를 렌더링, ignore 매칭, 하위 탐색 단계마다 다시 열거하면 파일 시스템 호출이 중복되고 각 단계가 서로 다른 시점의 목록을 관찰할 수 있습니다.
**조치:** 방문한 디렉토리마다 불변 `List<File>` 스냅샷과 ignore 결과를 한 번 만들고 세 단계에서 재사용하되, 직접 호출하는 기존 API에는 안전한 기본 인자를 유지합니다.
31 changes: 22 additions & 9 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,16 @@ fun go(topDir: String, maxLevel: Int) {

while(lle != null && Files.isDirectory(lle.file.toPath(), LinkOption.NOFOLLOW_LINKS)){
val currentLevel: Int = lle.level
// Capture one immutable directory snapshot for rendering, ignore matching,
// and traversal. This avoids repeated filesystem enumeration and keeps all
// decisions for a visited directory internally consistent.
val dirFiles = directory_snapshot(lle.file)
val exclude = process_ignore_file(lle.file, dirFiles)
if(maxLevel == -1 || currentLevel <= maxLevel)
process_dir(lle.file)
process_dir(lle.file, dirFiles, exclude)

if(maxLevel == -1 || currentLevel < maxLevel) {
val exclude = process_ignore_file(lle.file)
lle.file.listFiles()?.forEach {
dirFiles.forEach {
if(Files.isDirectory(it.toPath(), LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(it.toPath()) && it.name !in exclude) {
ll.push( LinkedListEntry(it, currentLevel+1))
}
Expand Down Expand Up @@ -121,7 +125,13 @@ fun String.urlEncodePath(): String {
return encoded.toString()
}

fun process_ignore_file(curr_dir: File): Set<String> {
fun directory_snapshot(curr_dir: File): List<File> =
curr_dir.listFiles()?.toList() ?: emptyList()

fun process_ignore_file(
curr_dir: File,
dirFiles: List<File> = directory_snapshot(curr_dir)
): Set<String> {

val ignore_filename = ".html4ignore"

Expand Down Expand Up @@ -152,8 +162,8 @@ fun process_ignore_file(curr_dir: File): Set<String> {
}

// ⚡ Bolt Performance Optimization: 디렉토리 목록을 Set에 추가하기 위해 필터링만 할 때는 정렬이 불필요하므로 .sorted()를 제거하여 O(N log N) 오버헤드를 방지합니다.
curr_dir.list()?.forEach {
val current = it
dirFiles.forEach {
val current = it.name
val pathCurrent = java.nio.file.Paths.get(current)
for (matcher in ignored_matchers) {
if (matcher.matches(pathCurrent)) {
Expand Down Expand Up @@ -185,9 +195,12 @@ fun write_index_file(curr_dir: File, content: String) {
}
}

fun process_dir(curr_dir: File){
fun process_dir(
curr_dir: File,
dirFiles: List<File> = directory_snapshot(curr_dir),
exclude: Set<String> = process_ignore_file(curr_dir, dirFiles)
){

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

val css = """
Expand Down Expand Up @@ -265,7 +278,7 @@ fun process_dir(curr_dir: File){
val index_middle = fun():String{
val l = StringBuilder()

val dir_files: MutableList<File> = curr_dir.listFiles()?.toMutableList() ?: mutableListOf()
val dir_files: MutableList<File> = dirFiles.toMutableList()
dir_files.sortWith(compareBy ({it.name}) )
dir_files.forEach {
val fileName = it.getName()
Expand Down
28 changes: 28 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -498,4 +498,32 @@ class MainTest {
// Line 1001 should be ignored due to line limit
assertFalse(excluded.contains("test.txt1001"))
}

@Test
fun testProcessIgnoreFileUsesProvidedDirectorySnapshot() {
File(tempDir, ".html4ignore").writeText("*.txt")
val visibleAtSnapshot = File(tempDir, "visible.txt")
visibleAtSnapshot.createNewFile()
val snapshot = tempDir.listFiles()?.toList() ?: emptyList()
File(tempDir, "created-later.txt").createNewFile()

val excluded = process_ignore_file(tempDir, snapshot)

assertTrue(excluded.contains("visible.txt"))
assertFalse(excluded.contains("created-later.txt"))
}

@Test
fun testProcessDirUsesProvidedDirectorySnapshot() {
val visibleAtSnapshot = File(tempDir, "visible.txt")
visibleAtSnapshot.createNewFile()
val snapshot = tempDir.listFiles()?.toList() ?: emptyList()
File(tempDir, "created-later.txt").createNewFile()

process_dir(tempDir, snapshot)

val html = File(tempDir, "index.html").readText()
assertTrue(html.contains("visible.txt"))
assertFalse(html.contains("created-later.txt"))
}
}
Loading