diff --git a/.jules/bolt.md b/.jules/bolt.md index 3b1d19ed..3ebd2a58 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()` 호출을 제거하여 성능을 최적화합니다. + +## 2024-05-18 - [디렉토리 목록 캐싱을 통한 I/O 오버헤드 최적화] +**Learning:** `process_dir` 및 `process_ignore_file`과 같은 함수에서 동일한 디렉토리에 대해 `listFiles()` 또는 `list()`를 반복적으로 호출하면, 파일 시스템 I/O로 인한 불필요한 성능 저하가 발생합니다. +**Action:** 디렉토리를 순회할 때 상위 루프에서 `listFiles()`를 한 번만 호출하여 캐싱한 후, 결과를 인자로 전달(예: `dirFiles` 배열)하여 중복된 파일 시스템 호출을 제거해야 합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index c14010fd..07b6ae93 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -51,12 +51,17 @@ 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: 디렉토리 목록을 캐싱하여 중복된 I/O 시스템 호출을 줄임 + val dirFiles = lle.file.listFiles() + val dirFilesNames = dirFiles?.map { it.name }?.toTypedArray() + val exclude = process_ignore_file(lle.file, dirFilesNames) + if(maxLevel == -1 || currentLevel <= maxLevel) - process_dir(lle.file) + process_dir(lle.file, exclude, dirFiles) 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 +126,7 @@ fun String.urlEncodePath(): String { return encoded.toString() } -fun process_ignore_file(curr_dir: File): Set { +fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): Set { val ignore_filename = ".html4ignore" @@ -152,7 +157,8 @@ fun process_ignore_file(curr_dir: File): Set { } // ⚡ Bolt Performance Optimization: 디렉토리 목록을 Set에 추가하기 위해 필터링만 할 때는 정렬이 불필요하므로 .sorted()를 제거하여 O(N log N) 오버헤드를 방지합니다. - curr_dir.list()?.forEach { + val list = dirFilesNames ?: curr_dir.list() + list?.forEach { val current = it val pathCurrent = java.nio.file.Paths.get(current) for (matcher in ignored_matchers) { @@ -185,9 +191,9 @@ fun write_index_file(curr_dir: File, content: String) { } } -fun process_dir(curr_dir: File){ +fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array? = null){ - val exclude: Set = process_ignore_file(curr_dir) + val exclude: Set = excludeSet ?: process_ignore_file(curr_dir) val styleNonce = generate_csp_nonce() val css = """ @@ -265,7 +271,8 @@ fun process_dir(curr_dir: File){ val index_middle = fun():String{ val l = StringBuilder() - val dir_files: MutableList = curr_dir.listFiles()?.toMutableList() ?: mutableListOf() + val filesList = dirFiles ?: curr_dir.listFiles() + val dir_files: MutableList = filesList?.toMutableList() ?: mutableListOf() 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 eaa855dd..9e8680f1 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, null) assertTrue(excluded.contains("test.txt")) assertTrue(excluded.contains("test.log")) @@ -123,11 +123,21 @@ class MainTest { @Test fun testProcessIgnoreFileNoIgnore() { - val excluded = process_ignore_file(tempDir) + val excluded = process_ignore_file(tempDir, null) assertTrue(excluded.contains("index.html")) assertEquals(9, excluded.size) // index.html + 8 default sensitive files } + @Test + fun testProcessIgnoreFileWithDirFilesNames() { + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText("test1.txt\ntest2.txt") + + val excluded = process_ignore_file(tempDir, arrayOf("test1.txt", "test3.txt")) + assertTrue(excluded.contains("index.html")) + assertEquals(10, excluded.size) // index.html + 8 default sensitive + test1.txt + } + @Test fun testProcessIgnoreFileInvalidRegex() { val ignoreFile = File(tempDir, ".html4ignore") @@ -136,7 +146,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, null) assertTrue(excluded.contains("test.log")) assertFalse(excluded.contains("test.txt")) @@ -349,7 +359,7 @@ 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, null) assertTrue(excluded.contains("index.html")) } @@ -389,7 +399,7 @@ class MainTest { File(tempDir, "test.txt").createNewFile() - val excluded = process_ignore_file(tempDir) + val excluded = process_ignore_file(tempDir, null) assertTrue(excluded.contains("test.txt")) } @@ -399,7 +409,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, null) assertTrue(excluded.contains("index.html")) } @@ -421,7 +431,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, null) assertTrue(excluded.contains("pattern500")) assertFalse(excluded.contains("pattern1005")) @@ -443,7 +453,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, null) assertFalse(excluded.contains("test.txt")) assertTrue(excluded.contains("index.html")) } @@ -458,7 +468,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, null) assertFalse(excluded.contains("test.txt")) assertTrue(excluded.contains("index.html")) } @@ -472,7 +482,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, null) // .log is excluded because it's valid assertTrue(excluded.contains("test.log")) // test.txt is not excluded because long regex was ignored @@ -492,7 +502,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, null) // Line 1000 should be processed assertTrue(excluded.contains("test.txt1000")) // Line 1001 should be ignored due to line limit