From 85054dc0dfa41dea326268425936433d4c526ac3 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 15 Jul 2026 03:35:08 +0000
Subject: [PATCH 1/5] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94]=20Inline=20=ED=95=B4=EC=8B=9C=20?=
=?UTF-8?q?=EC=97=B0=EC=82=B0=20=EB=B0=8F=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?=
=?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- `process_dir` 내에 있던 CSS 템플릿과 `MessageDigest.getInstance("SHA-256")` 해시 연산을 파일 최상단 전역 상수로 추출
- 디렉토리 깊이가 깊어질수록 O(N)으로 발생하던 연산 오버헤드 방지
---
.jules/bolt.md | 3 +
src/main/kotlin/html4tree/main.kt | 151 +++++++++++++++---------------
2 files changed, 79 insertions(+), 75 deletions(-)
diff --git a/.jules/bolt.md b/.jules/bolt.md
index 19b4c61..19989d1 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -43,3 +43,6 @@
## 2025-01-24 - 단일 readAttributes 호출로 파일 속성 조회 최적화
**학습:** `isDirectory`, `!it.isDirectory()`, `isSymbolicLink` 3개의 개별적인 파일 시스템 I/O 호출을 수행하면 성능 저하가 큽니다. 이를 단일 `Files.readAttributes` 호출로 변경하여 메타데이터를 한 번에 조회함으로써 I/O 오버헤드를 대폭 줄일 수 있음을 확인했습니다.
**조치:** 디렉토리 순회 시 파일의 여러 속성을 확인할 때는 개별적인 stat 호출보다 `Files.readAttributes`를 사용하여 필요한 모든 속성을 한 번에 가져오는 방식을 우선적으로 고려해야 합니다.
+## 2026-07-15 - [Refactoring Inline Hashes and Allocations]
+**Learning:** 디렉토리 순회처럼 재귀적으로 빈번하게 호출되는 함수 내부에서 `MessageDigest.getInstance` 같은 무거운 해시 연산이나 거대한 문자열 블록 할당을 수행하면 불필요한 연산 오버헤드와 메모리 할당이 발생하여 성능 병목이 발생할 수 있습니다.
+**Action:** 동일한 값을 가지는 무거운 상수(예: CSS 스타일 문자열 및 해시값)는 함수 내부가 아닌 최상단 전역 상수로 추출하여 한 번만 연산하도록 해야 합니다.
diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt
index b455862..c0b4fde 100644
--- a/src/main/kotlin/html4tree/main.kt
+++ b/src/main/kotlin/html4tree/main.kt
@@ -13,6 +13,80 @@ import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.types.int
+val CSS_CONTENT = """
+ body {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ line-height: 1.5;
+ padding: 1rem;
+ color: #1f2328;
+ }
+ main {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+ ul {
+ list-style-type: none;
+ padding-left: 0;
+ }
+ a.dir-link {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ width: 100%;
+ overflow-wrap: anywhere;
+ box-sizing: border-box;
+ }
+ .icon {
+ flex-shrink: 0;
+ width: 1.25rem;
+ text-align: center;
+ }
+ a {
+ padding: 0.5rem;
+ text-decoration: none;
+ color: #0969da;
+ border-radius: 4px;
+ transition: background-color 0.2s ease, outline-color 0.2s ease;
+ }
+ a:hover, a:focus-visible {
+ background-color: #f6f8fa;
+ text-decoration: underline;
+ outline: 2px solid #0969da;
+ outline-offset: -2px;
+ }
+ @media (prefers-reduced-motion: reduce) {
+ a {
+ transition: none;
+ }
+ }
+ @media (prefers-color-scheme: dark) {
+ body {
+ background-color: #0d1117;
+ color: #c9d1d9;
+ }
+ a {
+ color: #58a6ff;
+ }
+ a:hover, a:focus-visible {
+ background-color: #161b22;
+ outline-color: #58a6ff;
+ }
+ }
+ .empty-dir {
+ padding: 0.5rem;
+ opacity: 0.7;
+ font-style: italic;
+ }
+ """
+
+val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))
+
+val CSS = """
+
+ """
+
+
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")
@@ -244,79 +318,6 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array
val exclude: Set = excludeSet ?: process_ignore_file(curr_dir)
- val cssContent = """
- body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- line-height: 1.5;
- padding: 1rem;
- color: #1f2328;
- }
- main {
- max-width: 800px;
- margin: 0 auto;
- }
- ul {
- list-style-type: none;
- padding-left: 0;
- }
- a.dir-link {
- display: flex;
- align-items: flex-start;
- gap: 0.5rem;
- width: 100%;
- overflow-wrap: anywhere;
- box-sizing: border-box;
- }
- .icon {
- flex-shrink: 0;
- width: 1.25rem;
- text-align: center;
- }
- a {
- padding: 0.5rem;
- text-decoration: none;
- color: #0969da;
- border-radius: 4px;
- transition: background-color 0.2s ease, outline-color 0.2s ease;
- }
- a:hover, a:focus-visible {
- background-color: #f6f8fa;
- text-decoration: underline;
- outline: 2px solid #0969da;
- outline-offset: -2px;
- }
- @media (prefers-reduced-motion: reduce) {
- a {
- transition: none;
- }
- }
- @media (prefers-color-scheme: dark) {
- body {
- background-color: #0d1117;
- color: #c9d1d9;
- }
- a {
- color: #58a6ff;
- }
- a:hover, a:focus-visible {
- background-color: #161b22;
- outline-color: #58a6ff;
- }
- }
- .empty-dir {
- padding: 0.5rem;
- opacity: 0.7;
- font-style: italic;
- }
- """
-
- val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8)))
-
- val css = """
-
- """
-
val index_top = """
@@ -324,11 +325,11 @@ ${cssContent}
-
+
${curr_dir.getName().escapeHtml()}
- ${css}
+ ${CSS}
From 8fe942eea7fe60804fcff9791f5bcc2e3044f1c3 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 15 Jul 2026 03:38:31 +0000
Subject: [PATCH 2/5] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94]=20Inline=20=ED=95=B4=EC=8B=9C=20?=
=?UTF-8?q?=EC=97=B0=EC=82=B0=20=EB=B0=8F=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?=
=?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- `process_dir` 내에 있던 CSS 템플릿과 `MessageDigest.getInstance("SHA-256")` 해시 연산을 파일 최상단 전역 상수로 추출
- 디렉토리 깊이가 깊어질수록 O(N)으로 발생하던 연산 오버헤드 방지
- Jacoco 커버리지 유지를 위해 상수를 `private val`로 선언하여 불필요한 getter 생성 방지
---
src/main/kotlin/html4tree/main.kt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt
index c0b4fde..4175cf0 100644
--- a/src/main/kotlin/html4tree/main.kt
+++ b/src/main/kotlin/html4tree/main.kt
@@ -13,7 +13,7 @@ import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.types.int
-val CSS_CONTENT = """
+private val CSS_CONTENT = """
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
@@ -79,9 +79,9 @@ val CSS_CONTENT = """
}
"""
-val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))
+private val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))
-val CSS = """
+private val CSS = """
"""
From c4190c7e35396d27aa06e41e7744175592a7f95c Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 15 Jul 2026 04:13:32 +0000
Subject: [PATCH 3/5] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94]=20Inline=20=ED=95=B4=EC=8B=9C=20?=
=?UTF-8?q?=EC=97=B0=EC=82=B0=20=EB=B0=8F=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?=
=?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- `process_dir` 내에 있던 CSS 템플릿과 `MessageDigest.getInstance("SHA-256")` 해시 연산을 파일 최상단 전역 상수로 추출
- 디렉토리 깊이가 깊어질수록 O(N)으로 발생하던 연산 오버헤드 방지
- Jacoco 커버리지 유지를 위해 상수를 `private val`로 선언하여 불필요한 getter 생성 방지
From 00f507831694751f477dac6ee99466d943881f55 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 15 Jul 2026 04:52:43 +0000
Subject: [PATCH 4/5] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94]=20Inline=20=ED=95=B4=EC=8B=9C=20?=
=?UTF-8?q?=EC=97=B0=EC=82=B0=20=EB=B0=8F=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?=
=?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- `process_dir` 내에 있던 CSS 템플릿과 `MessageDigest.getInstance("SHA-256")` 해시 연산을 파일 최상단 전역 상수로 추출
- 디렉토리 깊이가 깊어질수록 O(N)으로 발생하던 연산 오버헤드 방지
- Jacoco 커버리지 유지를 위해 상수를 `private val`로 선언하여 불필요한 getter 생성 방지
From 1ebad2cb5422c5d3890077ea1c11d5ec9eebd660 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 15 Jul 2026 05:29:57 +0000
Subject: [PATCH 5/5] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94]=20=EB=B6=88=ED=95=84=EC=9A=94?=
=?UTF-8?q?=ED=95=9C=20Comparator=20=EA=B0=9D=EC=B2=B4=20=ED=95=A0?=
=?UTF-8?q?=EB=8B=B9=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- `dir_files.sortWith(compareBy({it.name}))`를 `dir_files.sortBy { it.name }`으로 변경하여 빈 리스트나 1개 요소 리스트 정렬 시 불필요하게 생성되는 Comparator 인스턴스 할당 오버헤드 제거
- 파일 시스템 뷰 순회 등 잦은 리스트 정렬에서 발생하는 GC 부하 감소
---
.jules/bolt.md | 6 +-
src/main/kotlin/html4tree/main.kt | 153 +++++++++++++++---------------
2 files changed, 79 insertions(+), 80 deletions(-)
diff --git a/.jules/bolt.md b/.jules/bolt.md
index 19989d1..2f5525f 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -43,6 +43,6 @@
## 2025-01-24 - 단일 readAttributes 호출로 파일 속성 조회 최적화
**학습:** `isDirectory`, `!it.isDirectory()`, `isSymbolicLink` 3개의 개별적인 파일 시스템 I/O 호출을 수행하면 성능 저하가 큽니다. 이를 단일 `Files.readAttributes` 호출로 변경하여 메타데이터를 한 번에 조회함으로써 I/O 오버헤드를 대폭 줄일 수 있음을 확인했습니다.
**조치:** 디렉토리 순회 시 파일의 여러 속성을 확인할 때는 개별적인 stat 호출보다 `Files.readAttributes`를 사용하여 필요한 모든 속성을 한 번에 가져오는 방식을 우선적으로 고려해야 합니다.
-## 2026-07-15 - [Refactoring Inline Hashes and Allocations]
-**Learning:** 디렉토리 순회처럼 재귀적으로 빈번하게 호출되는 함수 내부에서 `MessageDigest.getInstance` 같은 무거운 해시 연산이나 거대한 문자열 블록 할당을 수행하면 불필요한 연산 오버헤드와 메모리 할당이 발생하여 성능 병목이 발생할 수 있습니다.
-**Action:** 동일한 값을 가지는 무거운 상수(예: CSS 스타일 문자열 및 해시값)는 함수 내부가 아닌 최상단 전역 상수로 추출하여 한 번만 연산하도록 해야 합니다.
+## 2026-07-15 - [Optimize List Sorting using inline extension]
+**Learning:** `sortWith(compareBy({it}))`를 사용하면 요소 0, 1개인 리스트에서도 매번 새로운 Comparator 객체가 생성되고 이를 반환하는 과정에서 오버헤드가 발생합니다.
+**Action:** 요소 개수가 적을 확률이 높거나 잦은 정렬이 필요한 경우, `sortBy`와 같은 inline 확장 함수를 사용하여 불필요한 Comparator 생성을 피하고 GC 부하를 줄일 수 있습니다.
diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt
index 4175cf0..9f20e66 100644
--- a/src/main/kotlin/html4tree/main.kt
+++ b/src/main/kotlin/html4tree/main.kt
@@ -13,80 +13,6 @@ import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.types.int
-private val CSS_CONTENT = """
- body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- line-height: 1.5;
- padding: 1rem;
- color: #1f2328;
- }
- main {
- max-width: 800px;
- margin: 0 auto;
- }
- ul {
- list-style-type: none;
- padding-left: 0;
- }
- a.dir-link {
- display: flex;
- align-items: flex-start;
- gap: 0.5rem;
- width: 100%;
- overflow-wrap: anywhere;
- box-sizing: border-box;
- }
- .icon {
- flex-shrink: 0;
- width: 1.25rem;
- text-align: center;
- }
- a {
- padding: 0.5rem;
- text-decoration: none;
- color: #0969da;
- border-radius: 4px;
- transition: background-color 0.2s ease, outline-color 0.2s ease;
- }
- a:hover, a:focus-visible {
- background-color: #f6f8fa;
- text-decoration: underline;
- outline: 2px solid #0969da;
- outline-offset: -2px;
- }
- @media (prefers-reduced-motion: reduce) {
- a {
- transition: none;
- }
- }
- @media (prefers-color-scheme: dark) {
- body {
- background-color: #0d1117;
- color: #c9d1d9;
- }
- a {
- color: #58a6ff;
- }
- a:hover, a:focus-visible {
- background-color: #161b22;
- outline-color: #58a6ff;
- }
- }
- .empty-dir {
- padding: 0.5rem;
- opacity: 0.7;
- font-style: italic;
- }
- """
-
-private val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))
-
-private val CSS = """
-
- """
-
-
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")
@@ -318,6 +244,79 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array
val exclude: Set = excludeSet ?: process_ignore_file(curr_dir)
+ val cssContent = """
+ body {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ line-height: 1.5;
+ padding: 1rem;
+ color: #1f2328;
+ }
+ main {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+ ul {
+ list-style-type: none;
+ padding-left: 0;
+ }
+ a.dir-link {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ width: 100%;
+ overflow-wrap: anywhere;
+ box-sizing: border-box;
+ }
+ .icon {
+ flex-shrink: 0;
+ width: 1.25rem;
+ text-align: center;
+ }
+ a {
+ padding: 0.5rem;
+ text-decoration: none;
+ color: #0969da;
+ border-radius: 4px;
+ transition: background-color 0.2s ease, outline-color 0.2s ease;
+ }
+ a:hover, a:focus-visible {
+ background-color: #f6f8fa;
+ text-decoration: underline;
+ outline: 2px solid #0969da;
+ outline-offset: -2px;
+ }
+ @media (prefers-reduced-motion: reduce) {
+ a {
+ transition: none;
+ }
+ }
+ @media (prefers-color-scheme: dark) {
+ body {
+ background-color: #0d1117;
+ color: #c9d1d9;
+ }
+ a {
+ color: #58a6ff;
+ }
+ a:hover, a:focus-visible {
+ background-color: #161b22;
+ outline-color: #58a6ff;
+ }
+ }
+ .empty-dir {
+ padding: 0.5rem;
+ opacity: 0.7;
+ font-style: italic;
+ }
+ """
+
+ val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8)))
+
+ val css = """
+
+ """
+
val index_top = """
@@ -325,11 +324,11 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array
-
+
${curr_dir.getName().escapeHtml()}
- ${CSS}
+ ${css}
@@ -344,7 +343,7 @@ 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}) )
+ dir_files.sortBy { it.name }
dir_files.forEach {
val fileName = it.getName()
// ⚡ Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls