diff --git a/.jules/bolt.md b/.jules/bolt.md index 19b4c61..c8f37e8 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-21 - 반복 호출 함수에서 불변 문자열 및 비싼 연산 호이스팅(Hoisting) +**학습:** `process_dir`와 같이 자주 호출되는 함수 내부에서 상수 문자열(예: CSS 내용, HTML 템플릿 일부)을 정의하거나 비싼 연산(예: SHA-256 해시 계산)을 수행하면, 함수가 호출될 때마다 불필요한 메모리 할당과 CPU 오버헤드가 발생합니다. +**조치:** 불변 상수 문자열과 비싼 연산 결과를 최상위 `private val` 상수로 호이스팅(hoisting)하여 애플리케이션 시작 시 한 번만 계산되도록 최적화합니다. diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index b455862..2707977 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -24,6 +24,90 @@ class Html4tree : CliktCommand() { fun main(args: Array) = Html4tree().main(args) +// ⚡ Bolt Performance Optimization: Hoisted invariant static strings and expensive computations (SHA-256) +// out of frequently called functions to prevent redundant allocations and processing overhead on every directory traversal. +private 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") + +private 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; + } + """ + +private val styleHash = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(cssContent.toByteArray(Charsets.UTF_8))) + +private val css = """ + + """ + +private val index_bottom=""" + + + + + +""" internal data class FileIdentity(val key: Any?, val readable: Boolean) @@ -216,7 +300,6 @@ 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) // 보안 향상: .env, .git 등 민감한 정보가 포함될 수 있는 숨김 파일(.으로 시작하는 모든 항목)을 기본적으로 노출하지 않도록 제외 (정보 노출 방지) @@ -244,79 +327,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 = """ @@ -377,14 +387,6 @@ ${cssContent} return l.toString(); } - val index_bottom=""" - - - - - -""" - try { write_index_file(curr_dir, index_top+index_middle()+index_bottom) } catch (e: Exception) {