Skip to content
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@
## 2026-08-11 - Array의 toMutableList 할당 오버헤드 최적화
**학습:** 배열을 정렬하기 위해 `.toMutableList()`를 호출하면 새로운 `ArrayList` 객체와 내부 배열 객체가 할당되어 대규모 디렉토리를 순회할 때 가비지 컬렉션(GC) 부하를 유발합니다. 배열 복제가 필요한 경우 `.clone()`을 사용하면 하나의 배열 객체만 새로 할당되므로 더 효율적입니다.
**조치:** 디렉토리 파일 배열을 정렬하기 전에 복사할 때 `.toMutableList()` 대신 `.clone()`을 사용하여 불필요한 중간 컬렉션 할당을 제거하고 성능을 향상시켰습니다.

## 2024-05-18 - [디렉토리 목록 탐색 시 정적 리스트 Array 변환으로 성능 최적화]
**Learning:** `process_ignore_file` 내부에서 정적 리스트에 대한 `any` 순회 시 `List.any`는 매 순회마다 Iterator 객체를 할당합니다. 이러한 방식은 대규모 디렉토리를 탐색할 때 가비지 컬렉션(GC) 부하를 가중시킵니다.
**Action:** `Constants.defaultSensitiveExtensions`와 같은 정적인 `List`를 순회할 때는 `toTypedArray()`를 사용하여 미리 배열로 변환하고 `Array.any`를 사용하도록 최적화합니다. `Array.any`는 인라인 함수로 제공되어 컴파일 시 프리미티브 배열의 인덱스 기반 루프로 변환되므로 불필요한 Iterator 할당을 제거하고 성능을 크게 향상시킵니다.
6 changes: 4 additions & 2 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
it.isHiddenFile() ||
normalizedName in Constants.defaultSensitiveFileNamesLowercase ||
normalizedName.endsWith("~") ||
Constants.defaultSensitiveExtensions.any { extension ->
Constants.defaultSensitiveExtensionsArray.any { extension ->
normalizedName.endsWith(extension)
}
) {
Expand Down Expand Up @@ -404,7 +404,7 @@ fun write_index_file(
}

fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array<File>? = null){

val exclude: Set<String> = excludeSet ?: process_ignore_file(curr_dir)
val directoryName = curr_dir.name.ifEmpty { "Root" }

Expand Down Expand Up @@ -526,4 +526,6 @@ private object Constants {
".swo",
".swpx"
)

val defaultSensitiveExtensionsArray = defaultSensitiveExtensions.toTypedArray()
}
38 changes: 5 additions & 33 deletions src/main/kotlin/html4tree/util.kt
Original file line number Diff line number Diff line change
@@ -1,47 +1,19 @@
package html4tree

import java.io.File

data class Entry (val data: File, val level: Int, var next: Entry?, val fileKey: Any? = null)
import java.util.ArrayDeque

data class LinkedListEntry(val file: File, val level: Int, var fileKey: Any? = null)

class LinkedList {
var first: Entry? = null
var last: Entry? = null
private val deque = ArrayDeque<LinkedListEntry>()

fun push(lle: LinkedListEntry) {
if(last == null){
last = Entry(lle.file, lle.level, null, lle.fileKey)
first = last
} else {
val nextEntry = Entry(lle.file, lle.level, null, lle.fileKey)
val currentFirst = first
if (currentFirst == null) {
var currentLast = last!!
while (currentLast.next != null) {
currentLast = currentLast.next!!
}
currentLast.next = nextEntry
} else {
currentFirst.next = nextEntry
}
first = nextEntry
}
// Performance optimization: Using ArrayDeque avoids allocating Entry wrapper nodes
deque.addLast(lle)
}

fun pull(): LinkedListEntry? {
val l: Entry? = last
if(l != null) {
last = l.next
}

if(l == null){
return null
} else {
l.next = null
return LinkedListEntry(l.data, l.level, l.fileKey)
}
return deque.pollFirst()
}

}
64 changes: 0 additions & 64 deletions src/test/kotlin/html4tree/UtilTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,6 @@ class UtilTest {
assertNull(list.pull())
}

@Test
fun testEntryDataClass() {
val file1 = File("file1")
val entry1 = Entry(file1, 0, null)
val entry2 = Entry(file1, 0, null)

assertEquals(entry1, entry2)
assertEquals("Entry(data=file1, level=0, next=null, fileKey=null)", entry1.toString())
}

@Test
fun testEntryDataClassGeneratedMembers() {
val file1 = File("file1")
val entry = Entry(file1, 0, null)

assertEquals(entry, entry)
assertEquals(Entry(file1, 0, null).hashCode(), entry.hashCode())
assertNotEquals<Any>(entry, "not an entry")
assertNotEquals(entry, Entry(File("file2"), 0, null))
assertNotEquals(entry, Entry(file1, 1, null))
assertNotEquals(entry, Entry(file1, 0, Entry(file1, 1, null)))

val copied = entry.copy(level = 2)
assertEquals(file1, copied.data)
assertEquals(2, copied.level)
assertNull(copied.next)

val (data, level, next) = entry
assertEquals(file1, data)
assertEquals(0, level)
assertNull(next)
}

@Test
fun testLinkedListEntryDataClass() {
val file1 = File("file1")
Expand Down Expand Up @@ -110,25 +77,6 @@ class UtilTest {
assertEquals(File("f2"), entry2?.file)
}

@Test
fun testLinkedListAccessors() {
val list = LinkedList()
list.first = Entry(File("test"), 0, null)
list.last = Entry(File("test"), 0, null)
assertEquals(File("test"), list.first?.data)
assertEquals(File("test"), list.last?.data)
}

@Test
fun testLinkedListPushNullFirst() {
val list = LinkedList()
list.last = Entry(File("fake"), 0, null)
list.push(LinkedListEntry(File("f3"), 0))
assertEquals(File("fake"), list.pull()?.file)
assertEquals(File("f3"), list.pull()?.file)
assertEquals(File("f3"), list.first?.data)
}

@Test
fun testLinkedListPreservesFileKey() {
val key = Any()
Expand All @@ -141,16 +89,4 @@ class UtilTest {
assertEquals(1, pulled?.level)
assertEquals(key, pulled?.fileKey)
}

@Test
fun testLinkedListPushNullFirstWithExistingChain() {
val list = LinkedList()
list.last = Entry(File("f1"), 0, Entry(File("f2"), 0, null))
list.push(LinkedListEntry(File("f3"), 0))

assertEquals(File("f1"), list.pull()?.file)
assertEquals(File("f2"), list.pull()?.file)
assertEquals(File("f3"), list.pull()?.file)
assertNull(list.pull())
}
}
Loading