Skip to content
Merged
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()` 호출을 제거하여 성능을 최적화합니다.

## 2024-05-18 - [디렉토리 목록 캐싱을 통한 I/O 오버헤드 최적화]
**Learning:** `process_dir` 및 `process_ignore_file`과 같은 함수에서 동일한 디렉토리에 대해 `listFiles()` 또는 `list()`를 반복적으로 호출하면, 파일 시스템 I/O로 인한 불필요한 성능 저하가 발생합니다.
**Action:** 디렉토리를 순회할 때 상위 루프에서 `listFiles()`를 한 번만 호출하여 캐싱한 후, 결과를 인자로 전달(예: `dirFiles` 배열)하여 중복된 파일 시스템 호출을 제거해야 합니다.
23 changes: 15 additions & 8 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -121,7 +126,7 @@ fun String.urlEncodePath(): String {
return encoded.toString()
}

fun process_ignore_file(curr_dir: File): Set<String> {
fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): Set<String> {

val ignore_filename = ".html4ignore"

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

// ⚡ 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) {
Expand Down Expand Up @@ -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<String>? = null, dirFiles: Array<File>? = null){

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

val css = """
Expand Down Expand Up @@ -265,7 +271,8 @@ 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 filesList = dirFiles ?: curr_dir.listFiles()
val dir_files: MutableList<File> = filesList?.toMutableList() ?: mutableListOf()
dir_files.sortWith(compareBy ({it.name}) )
dir_files.forEach {
val fileName = it.getName()
Expand Down
32 changes: 21 additions & 11 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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")
Expand All @@ -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"))
Expand Down Expand Up @@ -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"))
}

Expand Down Expand Up @@ -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"))
}

Expand All @@ -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"))
}

Expand All @@ -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"))
Expand All @@ -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"))
}
Expand All @@ -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"))
}
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading