From c915ce617e6c571d3e416532fe916ae86b288737 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:21:35 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EC=9B=90=EC=8B=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=B0=95=EC=8B=B1=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EB=B0=8F=20StringBuilder=EB=A5=BC=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20Ledger=20=EC=A7=81=EB=A0=AC=ED=99=94=20=EC=B5=9C?= =?UTF-8?q?=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 | 30 ++++---- .../viewer/artifact/ArtifactLinkLedger.java | 70 ++++++++++--------- 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 284d180a7..e79fb9ca0 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-05 - Optimize ledger serialization with StringBuilder +**Learning:** Using `String.join("\t", ...)` for structuring ledgers with many fields (e.g., `ArtifactLinkLedger`, `KpiSnapshotLedger`) creates unnecessary string allocations and vararg array overhead. +**Action:** Use pre-sized `StringBuilder` and chained `.append()` to avoid string array allocations. Pass primitive types directly to `.append()` rather than wrapping in `String.valueOf()` to reduce temporary object allocations. diff --git a/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java b/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java index f5cbde5d5..e94142d67 100644 --- a/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java +++ b/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java @@ -148,20 +148,22 @@ 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()) - ); + // Optimization: Use pre-sized StringBuilder with chained appends to avoid + // varargs array creation and String.valueOf() allocations in String.join() + 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..b0650eb63 100644 --- a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java +++ b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java @@ -232,46 +232,48 @@ 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()) - ); + // Optimization: Use pre-sized StringBuilder with chained appends to avoid + // varargs array creation and String.valueOf() allocations in String.join() + 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) {