From f1daff2f0c33d3d270e8c17086cf171ff3a626dd Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:55:52 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EC=A4=91=EB=B3=B5=EB=90=9C?= =?UTF-8?q?=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=A0=9C=EA=B1=B0=EB=A1=9C=20I/O=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `go()`, `process_dir()`, `process_ignore_file()` 에서 각각 반복적으로 호출되던 `curr_dir.listFiles()` 및 `curr_dir.list()` 호출을 통합. - 디렉토리 내용을 한 번만 조회하고 파라미터로 전달하여, 값비싼 OS 레벨의 파일 시스템 I/O 접근 횟수를 디렉토리당 최대 4회에서 1회로 감소시킴. - Kotlin의 nullable 파라미터를 활용하여 기존 로직과 동일하게 동작하도록 유지함. - 테스트의 JaCoCo 분기 커버리지 100% 유지를 위해 default argument 대신 명시적 인자 전달 방식으로 테스트 코드 수정 완료. --- .jules/bolt.md | 3 +++ src/main/kotlin/html4tree/main.kt | 24 +++++++++--------- src/test/kotlin/html4tree/CoverageTest.kt | 2 +- src/test/kotlin/html4tree/MainTest.kt | 30 +++++++++++------------ 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 3b1d19ed..f7b7be29 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,3 +26,6 @@ ## 2024-07-28 - 디렉토리 목록 불필요한 정렬 오버헤드 **학습:** 디렉토리 목록(`list()` 또는 `listFiles()`)을 단순히 필터링하여 `Set`에 추가하는 경우처럼 특정 순서가 필요하지 않은 작업에서 `.sorted()`를 호출하면 불필요한 O(N log N) 오버헤드가 발생합니다. **조치:** `Set`과 같은 순서에 무관한 자료구조에 요소를 추가하기 위한 필터링 작업에서는 디렉토리 목록에서 `.sorted()` 호출을 제거하여 성능을 최적화합니다. +## 2024-07-12 - [Filesystem Access Optimization] +**Learning:** Calling `File.listFiles()` multiple times for the same directory is a significant performance bottleneck in deeply nested directory trees, causing expensive OS-level I/O operations per file or directory. This overhead compounds geometrically with the depth and width of the tree. +**Action:** Always fetch the directory contents once and pass the resulting array (`Array?`) down to helper methods (like sorting and filtering) to eliminate redundant filesystem reads. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index c14010fd..ad8307ee 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -51,12 +51,14 @@ fun go(topDir: String, maxLevel: Int) { while(lle != null && Files.isDirectory(lle.file.toPath(), LinkOption.NOFOLLOW_LINKS)){ val currentLevel: Int = lle.level + val dirFiles: Array? = lle.file.listFiles() + if(maxLevel == -1 || currentLevel <= maxLevel) - process_dir(lle.file) + process_dir(lle.file, dirFiles) if(maxLevel == -1 || currentLevel < maxLevel) { - val exclude = process_ignore_file(lle.file) - lle.file.listFiles()?.forEach { + val exclude = process_ignore_file(lle.file, dirFiles) + 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 +123,7 @@ fun String.urlEncodePath(): String { return encoded.toString() } -fun process_ignore_file(curr_dir: File): Set { +fun process_ignore_file(curr_dir: File, dirFiles: Array?): Set { val ignore_filename = ".html4ignore" @@ -152,8 +154,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 { file -> + val current = file.name val pathCurrent = java.nio.file.Paths.get(current) for (matcher in ignored_matchers) { if (matcher.matches(pathCurrent)) { @@ -185,9 +187,9 @@ fun write_index_file(curr_dir: File, content: String) { } } -fun process_dir(curr_dir: File){ +fun process_dir(curr_dir: File, dirFiles: Array?){ - val exclude: Set = process_ignore_file(curr_dir) + val exclude: Set = process_ignore_file(curr_dir, dirFiles) val styleNonce = generate_csp_nonce() val css = """ @@ -265,9 +267,9 @@ fun process_dir(curr_dir: File){ val index_middle = fun():String{ val l = StringBuilder() - val dir_files: MutableList = curr_dir.listFiles()?.toMutableList() ?: mutableListOf() - dir_files.sortWith(compareBy ({it.name}) ) - dir_files.forEach { + val dir_files_list: MutableList = dirFiles?.toMutableList() ?: mutableListOf() + dir_files_list.sortWith(compareBy ({it.name}) ) + dir_files_list.forEach { val fileName = it.getName() // ⚡ Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls if (fileName !in exclude) { diff --git a/src/test/kotlin/html4tree/CoverageTest.kt b/src/test/kotlin/html4tree/CoverageTest.kt index dccf3046..0868f0cb 100644 --- a/src/test/kotlin/html4tree/CoverageTest.kt +++ b/src/test/kotlin/html4tree/CoverageTest.kt @@ -12,7 +12,7 @@ class CoverageTest { readOnlyDir.mkdir() readOnlyDir.setWritable(false, false) try { - process_dir(readOnlyDir) + process_dir(readOnlyDir, readOnlyDir.listFiles()) // It should be handled securely assertTrue(true) } finally { diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index eaa855dd..d7a2ca55 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -113,7 +113,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()) assertTrue(excluded.contains("test.txt")) assertTrue(excluded.contains("test.log")) @@ -123,7 +123,7 @@ class MainTest { @Test fun testProcessIgnoreFileNoIgnore() { - val excluded = process_ignore_file(tempDir) + val excluded = process_ignore_file(tempDir, tempDir.listFiles()) assertTrue(excluded.contains("index.html")) assertEquals(9, excluded.size) // index.html + 8 default sensitive files } @@ -136,7 +136,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()) assertTrue(excluded.contains("test.log")) assertFalse(excluded.contains("test.txt")) @@ -150,7 +150,7 @@ class MainTest { File(tempDir, "test.ignore").createNewFile() File(tempDir, ".html4ignore").writeText("*.ignore") - process_dir(tempDir) + process_dir(tempDir, tempDir.listFiles()) val indexFile = File(tempDir, "index.html") assertTrue(indexFile.exists()) @@ -214,7 +214,7 @@ class MainTest { Assume.assumeTrue("Symlink creation not supported in this environment", false) } - process_dir(tempDir) + process_dir(tempDir, tempDir.listFiles()) assertEquals("original content", targetFile.readText()) assertTrue(indexFile.exists()) @@ -287,7 +287,7 @@ class MainTest { val notADirectory = File(tempDir, "not-a-directory") notADirectory.writeText("content") - process_dir(notADirectory) + process_dir(notADirectory, notADirectory.listFiles()) assertEquals("content", notADirectory.readText()) } @@ -349,14 +349,14 @@ 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()) assertTrue(excluded.contains("index.html")) } @Test fun testProcessDirItEqualsCurrDir() { File(tempDir, "tempDir").mkdir() - process_dir(tempDir) + process_dir(tempDir, tempDir.listFiles()) } @Test(expected = IllegalArgumentException::class) @@ -389,7 +389,7 @@ class MainTest { File(tempDir, "test.txt").createNewFile() - val excluded = process_ignore_file(tempDir) + val excluded = process_ignore_file(tempDir, tempDir.listFiles()) assertTrue(excluded.contains("test.txt")) } @@ -399,7 +399,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()) assertTrue(excluded.contains("index.html")) } @@ -421,7 +421,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()) assertTrue(excluded.contains("pattern500")) assertFalse(excluded.contains("pattern1005")) @@ -443,7 +443,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()) assertFalse(excluded.contains("test.txt")) assertTrue(excluded.contains("index.html")) } @@ -458,7 +458,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()) assertFalse(excluded.contains("test.txt")) assertTrue(excluded.contains("index.html")) } @@ -472,7 +472,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()) // .log is excluded because it's valid assertTrue(excluded.contains("test.log")) // test.txt is not excluded because long regex was ignored @@ -492,7 +492,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()) // Line 1000 should be processed assertTrue(excluded.contains("test.txt1000")) // Line 1001 should be ignored due to line limit