Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@
## 2025-01-24 - 단일 readAttributes 호출로 파일 속성 조회 최적화 (순회 루프)
**학습:** 디렉토리 순회 루프 내에서 isDirectory 및 isSymbolicLink 두 번의 stat을 각각 호출하면 파일 시스템 I/O 오버헤드가 배가됩니다. 메모리 내 제외 규칙 확인 후 한 번의 readAttributes로 속성을 한 번에 가져오는 것이 훨씬 빠릅니다.
**조치:** Files.isDirectory 및 Files.isSymbolicLink를 단일 Files.readAttributes 호출로 교체하여 O(N) I/O 통신을 최적화했습니다.
## 2025-01-25 - 호이스팅(Hoisting) 불변 정적 컬렉션 및 비교자
**학습:** 디렉토리 순회와 같이 자주 호출되는 함수(`process_ignore_file`, `process_dir`) 내에서 `listOf`로 불변 리스트를 생성하거나, `compareBy` 등을 통해 매번 새로운 `Comparator` 객체를 할당하는 것은 가비지 컬렉션(GC) 오버헤드를 발생시키고 성능을 저하시킵니다.
**조치:** 불변 데이터 컬렉션과 컴패레이터 인스턴스를 최상단 수준의 `private val` 상수로 끌어올려(Hoisting), 여러 호출 간에 한 번만 생성된 객체를 재사용하도록 최적화했습니다. `private val`로 선언하면 불필요한 getter 메서드 생성을 방지하여 코드 커버리지(Jacoco)도 안전하게 유지할 수 있습니다.
11 changes: 8 additions & 3 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ li + li {

private val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))

// ⚡ Bolt Performance Optimization: Hoist invariant collections and comparators to top-level constants
// to prevent redundant allocations during directory traversal. Declared as private val to avoid getter generation.
private val DEFAULT_SENSITIVE_FILES = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json")
private val FILE_NAME_COMPARATOR = compareBy<File> { it.name }

class Html4tree : CliktCommand() {
val maxLevel:Int by option(help="Number of levels deep for which to generate an index.html file", hidden = false).int().default(-1)
val topDir: String by argument(help="Top directory to crawl")
Expand Down Expand Up @@ -314,8 +319,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
files_to_exclude.add("index.html")

// 보안 향상: 민감한 시스템, 설정, 시크릿 파일을 디렉토리 목록에서 기본적으로 제외하여 정보 노출(Information Exposure) 방지
val defaultSensitiveFiles = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json")
files_to_exclude.addAll(defaultSensitiveFiles)
files_to_exclude.addAll(DEFAULT_SENSITIVE_FILES)

// 보안 향상: dot-like prefixes are treated as hidden to prevent visually-confusable sensitive entries from reaching generated indexes.
(dirFilesNames ?: curr_dir.list())?.forEach {
Expand Down Expand Up @@ -368,7 +372,8 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array

val filesList = dirFiles ?: curr_dir.listFiles()
val dir_files: MutableList<File> = filesList?.toMutableList() ?: mutableListOf()
dir_files.sortWith(compareBy ({it.name}) )
// ⚡ Bolt Performance Optimization: Use hoisted comparator to prevent redundant Comparator allocations
dir_files.sortWith(FILE_NAME_COMPARATOR)
dir_files.forEach {
val fileName = it.getName()
// ⚡ Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls
Expand Down
46 changes: 46 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.system.measureTimeMillis
import java.lang.management.ManagementFactory

class MainTest {
private lateinit var tempDir: File
Expand Down Expand Up @@ -717,4 +719,48 @@ class MainTest {
assertFalse(processed, "fileKey mismatch should skip directory processing")
assertFalse(listed, "fileKey mismatch should skip child listing")
}

@Test
fun testCrawlPerformanceBenchmark() {
val levels = 3
val dirsPerLevel = 3
val filesPerDir = 5

fun createTree(currentDir: File, currentLevel: Int) {
if (currentLevel > levels) return
for (i in 1..filesPerDir) {
File(currentDir, "file-${currentLevel}-${i}.txt").createNewFile()
}
for (i in 1..dirsPerLevel) {
val sub = File(currentDir, "dir-${currentLevel}-${i}")
sub.mkdir()
createTree(sub, currentLevel + 1)
}
}

val benchmarkDir = File(tempDir, "benchmark")
benchmarkDir.mkdir()
createTree(benchmarkDir, 1)

val garbageCollectorBeans = ManagementFactory.getGarbageCollectorMXBeans()
var initialGcCount: Long = 0
for (gc in garbageCollectorBeans) {
val count = gc.collectionCount
if (count > 0) initialGcCount += count
}

val time = measureTimeMillis {
go(benchmarkDir.absolutePath, -1)
}

var finalGcCount: Long = 0
for (gc in garbageCollectorBeans) {
val count = gc.collectionCount
if (count > 0) finalGcCount += count
}

println("Benchmark Tree Crawl Time: ${time}ms")
println("Benchmark GC Collections: ${finalGcCount - initialGcCount}")
assertTrue(time >= 0, "Time should be positive")
}
}
Loading