From 45ca61ed73094602d65c993d26aac29a0eefb4ef Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Thu, 1 Oct 2026 21:41:47 +0000
Subject: [PATCH 1/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.join()=EC=9D=84?=
=?UTF-8?q?=20StringBuilder=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=98?=
=?UTF-8?q?=EC=97=AC=20=EC=9B=90=EC=9E=A5=20=EC=A7=81=EB=A0=AC=ED=99=94=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.jules/bolt.md | 3 +
.../viewer/analytics/KpiSnapshotLedger.java | 28 ++++----
.../viewer/artifact/ArtifactLinkLedger.java | 68 +++++++++----------
3 files changed, 51 insertions(+), 48 deletions(-)
diff --git a/.jules/bolt.md b/.jules/bolt.md
index 284d180a7..f2b2e8e3a 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -19,3 +19,6 @@
## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당)
**Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다.
**Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다.
+## 2026-10-01 - Ledger Serialization Optimization
+**Learning:** Using String.join with many arguments creates unnecessary varargs arrays and intermediate objects. Since the field serialization in Ledger appends many predefined strings and tabs, switching to a pre-sized StringBuilder drastically reduces intermediate object allocations.
+**Action:** Replace String.join(...) with pre-sized StringBuilder.append(...) chains for hot-path ledger serialization where varargs instantiation creates overhead.
diff --git a/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java b/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java
index f5cbde5d5..144462ccf 100644
--- a/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java
+++ b/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java
@@ -148,20 +148,20 @@ private void appendLine(String line) {
}
private static String serialize(KpiSnapshotRecord record) {
- return String.join("\t",
- SNAPSHOT,
- field(record.tenantId()),
- field(record.subjectId()),
- field(record.exportedAt()),
- String.valueOf(record.totalJobs()),
- String.valueOf(record.submittedJobs()),
- String.valueOf(record.processingJobs()),
- String.valueOf(record.succeededJobs()),
- String.valueOf(record.failedJobs()),
- String.valueOf(record.deadLetteredJobs()),
- String.valueOf(record.conversionSuccessRate()),
- field(record.p95TimeToPreviewMs())
- );
+ return new StringBuilder(256)
+ .append(SNAPSHOT).append('\t')
+ .append(field(record.tenantId())).append('\t')
+ .append(field(record.subjectId())).append('\t')
+ .append(field(record.exportedAt())).append('\t')
+ .append(record.totalJobs()).append('\t')
+ .append(record.submittedJobs()).append('\t')
+ .append(record.processingJobs()).append('\t')
+ .append(record.succeededJobs()).append('\t')
+ .append(record.failedJobs()).append('\t')
+ .append(record.deadLetteredJobs()).append('\t')
+ .append(record.conversionSuccessRate()).append('\t')
+ .append(field(record.p95TimeToPreviewMs()))
+ .toString();
}
private static String field(String value) {
diff --git a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java
index 897b95b2e..a59d5ac03 100644
--- a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java
+++ b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java
@@ -232,46 +232,46 @@ private void appendLine(String line) {
}
private static String serializeIssued(ArtifactLinkRecord record) {
- return String.join("\t",
- ISSUED,
- field(record.tokenId()),
- field(record.tenantId()),
- field(record.subjectId()),
- record.docId().toString(),
- field(record.scope()),
- field(record.purpose()),
- field(record.artifactChecksum()),
- field(record.viewerSessionId()),
- field(record.issuedAt()),
- field(record.expiresAt()),
- field(record.revokedAt()),
- field(record.revokedBy()),
- field(record.revokeReason())
- );
+ return new StringBuilder(256)
+ .append(ISSUED).append('\t')
+ .append(field(record.tokenId())).append('\t')
+ .append(field(record.tenantId())).append('\t')
+ .append(field(record.subjectId())).append('\t')
+ .append(record.docId().toString()).append('\t')
+ .append(field(record.scope())).append('\t')
+ .append(field(record.purpose())).append('\t')
+ .append(field(record.artifactChecksum())).append('\t')
+ .append(field(record.viewerSessionId())).append('\t')
+ .append(field(record.issuedAt())).append('\t')
+ .append(field(record.expiresAt())).append('\t')
+ .append(field(record.revokedAt())).append('\t')
+ .append(field(record.revokedBy())).append('\t')
+ .append(field(record.revokeReason()))
+ .toString();
}
private static String serializeRevoked(ArtifactLinkRecord record) {
- return String.join("\t",
- REVOKED,
- field(record.tokenId()),
- field(record.revokedAt()),
- field(record.revokedBy()),
- field(record.revokeReason())
- );
+ return new StringBuilder(128)
+ .append(REVOKED).append('\t')
+ .append(field(record.tokenId())).append('\t')
+ .append(field(record.revokedAt())).append('\t')
+ .append(field(record.revokedBy())).append('\t')
+ .append(field(record.revokeReason()))
+ .toString();
}
private static String serializeRead(ArtifactReadEvent event) {
- return String.join("\t",
- READ,
- field(event.tenantId()),
- field(event.subjectId()),
- event.docId().toString(),
- field(event.tokenId()),
- field(event.rangeRequested()),
- String.valueOf(event.statusCode()),
- field(event.traceId()),
- field(event.readAt())
- );
+ return new StringBuilder(256)
+ .append(READ).append('\t')
+ .append(field(event.tenantId())).append('\t')
+ .append(field(event.subjectId())).append('\t')
+ .append(event.docId().toString()).append('\t')
+ .append(field(event.tokenId())).append('\t')
+ .append(field(event.rangeRequested())).append('\t')
+ .append(event.statusCode()).append('\t')
+ .append(field(event.traceId())).append('\t')
+ .append(field(event.readAt()))
+ .toString();
}
private static String field(String value) {
From 4effafcd0f54fb755e85ea88cfc5192ca47be4c6 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Thu, 1 Oct 2026 21:53:00 +0000
Subject: [PATCH 2/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.join()=EC=9D=84?=
=?UTF-8?q?=20StringBuilder=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=98?=
=?UTF-8?q?=EC=97=AC=20=EC=9B=90=EC=9E=A5=20=EC=A7=81=EB=A0=AC=ED=99=94=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
From 70df717afcabb2fcde0aea83fe887a725151b8ee Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Thu, 1 Oct 2026 21:57:24 +0000
Subject: [PATCH 3/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.join()=EC=9D=84?=
=?UTF-8?q?=20StringBuilder=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=98?=
=?UTF-8?q?=EC=97=AC=20=EC=9B=90=EC=9E=A5=20=EC=A7=81=EB=A0=AC=ED=99=94=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20=EC=B7=A8=EC=95=BD?=
=?UTF-8?q?=EC=A0=90=20=ED=8C=A8=EC=B9=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index af3ec7ad2..1e5de5f50 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,7 +43,7 @@
OSV config contains a narrow temporary ignore because OSV Scanner
v2.3.8 still reports 2.22.1 even though the advisory text names it as
patched. -->
- 2.22.1
+ 2.22.3
1.5.35
From b419c15ccf8018507a9d1ef0410605170617bdaa Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 2 Oct 2026 07:05:55 +0900
Subject: [PATCH 4/6] chore: isolate ledger serialization delta
---
.jules/bolt.md | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.jules/bolt.md b/.jules/bolt.md
index f2b2e8e3a..284d180a7 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -19,6 +19,3 @@
## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당)
**Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다.
**Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다.
-## 2026-10-01 - Ledger Serialization Optimization
-**Learning:** Using String.join with many arguments creates unnecessary varargs arrays and intermediate objects. Since the field serialization in Ledger appends many predefined strings and tabs, switching to a pre-sized StringBuilder drastically reduces intermediate object allocations.
-**Action:** Replace String.join(...) with pre-sized StringBuilder.append(...) chains for hot-path ledger serialization where varargs instantiation creates overhead.
From 47dda5c25926bf73a8f5bcc978f9e85b8cce4485 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 2 Oct 2026 07:06:09 +0900
Subject: [PATCH 5/6] chore: isolate ledger serialization delta
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 1e5de5f50..af3ec7ad2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,7 +43,7 @@
OSV config contains a narrow temporary ignore because OSV Scanner
v2.3.8 still reports 2.22.1 even though the advisory text names it as
patched. -->
- 2.22.3
+ 2.22.1
1.5.35
From 62e3f787d58ce07d77fa74ccc91f5395a638ee33 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Thu, 1 Oct 2026 22:35:38 +0000
Subject: [PATCH 6/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.join()=EC=9D=84?=
=?UTF-8?q?=20StringBuilder=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=98?=
=?UTF-8?q?=EC=97=AC=20=EC=9B=90=EC=9E=A5=20=EC=A7=81=EB=A0=AC=ED=99=94=20?=
=?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20=EC=B7=A8=EC=95=BD?=
=?UTF-8?q?=EC=A0=90=20=ED=8C=A8=EC=B9=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.jules/bolt.md | 3 +++
pom.xml | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/.jules/bolt.md b/.jules/bolt.md
index 284d180a7..f2b2e8e3a 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -19,3 +19,6 @@
## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당)
**Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다.
**Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다.
+## 2026-10-01 - Ledger Serialization Optimization
+**Learning:** Using String.join with many arguments creates unnecessary varargs arrays and intermediate objects. Since the field serialization in Ledger appends many predefined strings and tabs, switching to a pre-sized StringBuilder drastically reduces intermediate object allocations.
+**Action:** Replace String.join(...) with pre-sized StringBuilder.append(...) chains for hot-path ledger serialization where varargs instantiation creates overhead.
diff --git a/pom.xml b/pom.xml
index af3ec7ad2..1e5de5f50 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,7 +43,7 @@
OSV config contains a narrow temporary ignore because OSV Scanner
v2.3.8 still reports 2.22.1 even though the advisory text names it as
patched. -->
- 2.22.1
+ 2.22.3
1.5.35