Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@
## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당)
**Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다.
**Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다.

## 2024-10-14 - Optimizing DOM mutations with DocumentFragment
**Learning:** In frontend performance optimization, replacing loop-based `appendChild` calls with a `DocumentFragment` reduces redundant reflows. When optimizing utility functions (like `addDetailRow`), all internal call sites must be tracked down and updated to accept the fragment object to avoid `ReferenceError` crashes. Additionally, custom mock DOM setups in test files must implement a `MockDocumentFragment` that correctly pushes its `childNodes` upon `appendChild` in the parent node mock, instead of appending the fragment itself, simulating standard browser DOM behavior.
**Action:** When migrating UI renders to use `DocumentFragment`, always use tools like `grep -rn` to verify all utility call sites are updated, and ensure the mock environment fully supports `DocumentFragment` unpacking in `appendChild` before claiming victory.
35 changes: 21 additions & 14 deletions src/main/resources/static/assets/viewer/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ function renderHistory(history = loadHistory()) {
el.historyBody.textContent = "";
el.emptyHistory.hidden = history.length > 0;

const fragment = document.createDocumentFragment();

for (const job of history) {
const row = document.createElement("tr");
const fileCell = document.createElement("td");
Expand Down Expand Up @@ -143,18 +145,19 @@ function renderHistory(history = loadHistory()) {
}

row.append(fileCell, statusCell, submittedCell, actionsCell);
el.historyBody.appendChild(row);
fragment.appendChild(row);
}

el.historyBody.appendChild(fragment);
renderRecoveryEvidence(history);
}

function addDetailRow(label, value) {
function addDetailRow(fragment, label, value) {
const term = document.createElement("dt");
const description = document.createElement("dd");
term.textContent = label;
description.textContent = formatDetailValue(value);
el.jobDetailBody.append(term, description);
fragment.append(term, description);
}

function formatDetailValue(value) {
Expand All @@ -177,17 +180,21 @@ function renderJobDetail(detail) {
: "Operational evidence";
el.jobDetailBody.textContent = "";

addDetailRow("Job ID", detail.jobId);
addDetailRow("Tenant", detail.tenantId);
addDetailRow("Status", detail.status);
addDetailRow("Message", detail.message);
addDetailRow("Attempts", `${detail.attemptCount ?? 0} / ${detail.maxAttempts ?? "n/a"}`);
addDetailRow("Dead-lettered", Boolean(detail.deadLettered));
addDetailRow("Retry at", detail.retryAt);
addDetailRow("Created", detail.createdAt);
addDetailRow("Started", detail.startedAt);
addDetailRow("Completed", detail.completedAt);
addDetailRow("Artifact", detail.convertedResourcePath);
const fragment = document.createDocumentFragment();

addDetailRow(fragment, "Job ID", detail.jobId);
addDetailRow(fragment, "Tenant", detail.tenantId);
addDetailRow(fragment, "Status", detail.status);
addDetailRow(fragment, "Message", detail.message);
addDetailRow(fragment, "Attempts", `${detail.attemptCount ?? 0} / ${detail.maxAttempts ?? "n/a"}`);
addDetailRow(fragment, "Dead-lettered", Boolean(detail.deadLettered));
addDetailRow(fragment, "Retry at", detail.retryAt);
addDetailRow(fragment, "Created", detail.createdAt);
addDetailRow(fragment, "Started", detail.startedAt);
addDetailRow(fragment, "Completed", detail.completedAt);
addDetailRow(fragment, "Artifact", detail.convertedResourcePath);

el.jobDetailBody.appendChild(fragment);

el.retryJobBtn.hidden = !detail.deadLettered;
}
Expand Down
5 changes: 4 additions & 1 deletion src/test/js/demo-integration.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";

import { MockElement } from "./mock-dom.mjs";
import { MockElement, MockDocumentFragment } from "./mock-dom.mjs";

const elementIds = [
"upload-form",
Expand Down Expand Up @@ -54,6 +54,9 @@ test("the executable demo renders inert actions and blocks repeated status activ
createElement(tagName) {
return new MockElement(tagName);
},
createDocumentFragment() {
return new MockDocumentFragment();
},
};
globalThis.window = {
confirm() {
Expand Down
5 changes: 4 additions & 1 deletion src/test/js/dom-utils.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {
createLink,
setBusyState
} from "../../main/resources/static/assets/viewer/dom-utils.js";
import { MockElement, MockTextNode } from "./mock-dom.mjs";
import { MockElement, MockTextNode, MockDocumentFragment } from "./mock-dom.mjs";

globalThis.document = {
createElement(tagName) {
return new MockElement(tagName);
},
createDocumentFragment() {
return new MockDocumentFragment();
}
};

Expand Down
41 changes: 40 additions & 1 deletion src/test/js/mock-dom.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
export class MockDocumentFragment {
constructor() {
this.childNodes = [];
}

appendChild(node) {
if (node instanceof MockDocumentFragment) {
this.childNodes.push(...node.childNodes);
node.childNodes = [];
return node;
}
this.childNodes.push(node);
return node;
}

append(...nodes) {
for (const node of nodes) {
if (node instanceof MockDocumentFragment) {
this.childNodes.push(...node.childNodes);
node.childNodes = [];
} else {
this.childNodes.push(node);
}
}
}
}

export class MockTextNode {
constructor(text) {
this.type = "text";
Expand Down Expand Up @@ -31,12 +58,24 @@ export class MockElement {
}

appendChild(node) {
if (node instanceof MockDocumentFragment) {
this.childNodes.push(...node.childNodes);
node.childNodes = [];
return node;
}
this.childNodes.push(node);
return node;
}

append(...nodes) {
this.childNodes.push(...nodes);
for (const node of nodes) {
if (node instanceof MockDocumentFragment) {
this.childNodes.push(...node.childNodes);
node.childNodes = [];
} else {
this.childNodes.push(node);
}
}
}

replaceChildren(...nodes) {
Expand Down
Loading