From 2c1c96371105466cc80e0fba6f4d6ad9cae69c91 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:19:11 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0]=20=EB=A3=A8=ED=94=84=20=EB=82=B4=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=95=A0=EB=8B=B9=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 3 +++ src/main/kotlin/html4tree/main.kt | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 165882d..4229136 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -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)도 안전하게 유지할 수 있습니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 8942c04..e7ff805 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -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 { 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") @@ -314,8 +319,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = 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 { @@ -368,7 +372,8 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array val filesList = dirFiles ?: curr_dir.listFiles() val dir_files: MutableList = 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 From 34170e2b77e34d9c1a859acae5e38b0620aba820 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:18:29 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0]=20=EC=84=B1=EB=8A=A5=20=EC=B8=A1=EC=A0=95?= =?UTF-8?q?=EC=9A=A9=20Benchmark=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/kotlin/html4tree/MainTest.kt | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt index 179b6c5..3cd055d 100644 --- a/src/test/kotlin/html4tree/MainTest.kt +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -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 @@ -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") + } }