From 7bcf23fb979a23b84daae2475438862f4cfd2f33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 13 Jul 2026 02:30:11 +0900 Subject: [PATCH] perf: reuse directory snapshots during traversal --- .jules/bolt.md | 4 ++++ src/main/kotlin/html4tree/main.kt | 31 +++++++++++++++++++-------- src/test/kotlin/html4tree/MainTest.kt | 28 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 3b1d19e..96fefb5 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,3 +26,7 @@ ## 2024-07-28 - 디렉토리 목록 불필요한 정렬 오버헤드 **학습:** 디렉토리 목록(`list()` 또는 `listFiles()`)을 단순히 필터링하여 `Set`에 추가하는 경우처럼 특정 순서가 필요하지 않은 작업에서 `.sorted()`를 호출하면 불필요한 O(N log N) 오버헤드가 발생합니다. **조치:** `Set`과 같은 순서에 무관한 자료구조에 요소를 추가하기 위한 필터링 작업에서는 디렉토리 목록에서 `.sorted()` 호출을 제거하여 성능을 최적화합니다. + +## 2026-07-13 - 디렉토리 스냅샷 재사용 +**학습:** 동일 디렉토리를 렌더링, ignore 매칭, 하위 탐색 단계마다 다시 열거하면 파일 시스템 호출이 중복되고 각 단계가 서로 다른 시점의 목록을 관찰할 수 있습니다. +**조치:** 방문한 디렉토리마다 불변 `List` 스냅샷과 ignore 결과를 한 번 만들고 세 단계에서 재사용하되, 직접 호출하는 기존 API에는 안전한 기본 인자를 유지합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index c14010f..ceccdad 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -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)) } @@ -121,7 +125,13 @@ fun String.urlEncodePath(): String { return encoded.toString() } -fun process_ignore_file(curr_dir: File): Set { +fun directory_snapshot(curr_dir: File): List = + curr_dir.listFiles()?.toList() ?: emptyList() + +fun process_ignore_file( + curr_dir: File, + dirFiles: List = directory_snapshot(curr_dir) +): Set { val ignore_filename = ".html4ignore" @@ -152,8 +162,8 @@ fun process_ignore_file(curr_dir: File): Set { } // ⚡ 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)) { @@ -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 = directory_snapshot(curr_dir), + exclude: Set = process_ignore_file(curr_dir, dirFiles) +){ - val exclude: Set = process_ignore_file(curr_dir) val styleNonce = generate_csp_nonce() val css = """ @@ -265,7 +278,7 @@ fun process_dir(curr_dir: File){ val index_middle = fun():String{ val l = StringBuilder() - val dir_files: MutableList = curr_dir.listFiles()?.toMutableList() ?: mutableListOf() + val dir_files: MutableList = dirFiles.toMutableList() dir_files.sortWith(compareBy ({it.name}) ) dir_files.forEach { val fileName = it.getName() diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index eaa855d..f11ac0c 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -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")) + } }