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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
## 2026-07-13 - ๋‹จ์ผ ํŒจ์Šค ๋ฌธ์ž์—ด ์น˜ํ™˜ ์ตœ์ ํ™” (O(N) ๋‹จ์ผ ์Šค์บ” ๋ฐ ์ง€์—ฐ ํ• ๋‹น)
**Learning:** `String.replace()`๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฒด์ด๋‹ํ•˜์—ฌ ํ˜ธ์ถœํ•˜๋ฉด, ๋ฌธ์ž์—ด ์น˜ํ™˜์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋„ ๋‚ด๋ถ€์ ์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ์Šค์บ”์ด ์ค‘๋ณต ๋ฐœ์ƒํ•˜๊ณ , ์น˜ํ™˜ ์‹œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ๋ฌธ์ž์—ด ๊ฐ์ฒด์™€ char ๋ฐฐ์—ด์ด ํ• ๋‹น๋˜์–ด ๋ฉ”๋ชจ๋ฆฌ ๋‚ญ๋น„์™€ ์„ฑ๋Šฅ ์ €ํ•˜(GC ์••๋ฐ•)๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
**Action:** ์—ฌ๋Ÿฌ ๋ฌธ์ž๋ฅผ ํ•œ ๋ฒˆ์— ์น˜ํ™˜ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, O(N) ๋‹จ์ผ ์Šค์บ”์„ ํ†ตํ•ด `charAt()`์œผ๋กœ ๋ฌธ์ž๋ฅผ ํ™•์ธํ•˜๊ณ , ์น˜ํ™˜์ด ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ `StringBuilder`๋ฅผ ์ง€์—ฐ ํ• ๋‹น(Lazy allocation)ํ•˜์—ฌ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น์„ ๋ฐฉ์ง€ํ•œ๋‹ค.
## 2026-10-06 - DOM ๋ Œ๋”๋ง ์ตœ์ ํ™”: DocumentFragment๋ฅผ ํ†ตํ•œ ์ผ๊ด„ ์ฒ˜๋ฆฌ (Batching)
**Learning:** `demo.js`์˜ `renderHistory`์™€ `renderJobDetail`์—์„œ ๋ฐฐ์—ด์ด๋‚˜ ์š”์†Œ๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ ๊ฐœ๋ณ„์ ์œผ๋กœ DOM์— `appendChild`๋‚˜ `append`๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด, ๋งค ์‚ฝ์ž…๋งˆ๋‹ค ๋ธŒ๋ผ์šฐ์ €์˜ ๋ฆฌํ”Œ๋กœ์šฐ(Reflow)์™€ ๋ฆฌํŽ˜์ธํŠธ(Repaint)๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜์–ด ๋ Œ๋”๋ง ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
**Action:** ์—ฌ๋Ÿฌ DOM ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ๋Š” ํ•ญ์ƒ `document.createDocumentFragment()`๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ์ƒ์—์„œ ๋ชจ๋“  ์ž์‹ ๋…ธ๋“œ๋ฅผ ๋ถ€์ฐฉํ•œ ํ›„, ์ตœ์ข…์ ์œผ๋กœ ์‹ค์ œ DOM์— ๋‹จ ํ•œ ๋ฒˆ๋งŒ ์‚ฝ์ž…ํ•˜๋„๋ก ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.
40 changes: 26 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,10 @@ function renderHistory(history = loadHistory()) {
el.historyBody.textContent = "";
el.emptyHistory.hidden = history.length > 0;

// โšก Bolt: Batch DOM row insertions using DocumentFragment
// Impact: Prevents N browser reflows/repaints by building the entire list in memory, then flushing to DOM in 1 operation.
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 +147,20 @@ 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 +183,23 @@ 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);
// โšก Bolt: Batch detail row DOM insertions using DocumentFragment
// Impact: Prevents N browser reflows/repaints for each detail attribute, writing to the DOM once per panel open.
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
10 changes: 8 additions & 2 deletions 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 @@ -51,6 +51,9 @@ test("the executable demo renders inert actions and blocks repeated status activ
getElementById(id) {
return elements.get(id);
},
createDocumentFragment() {
return new MockDocumentFragment();
},
createElement(tagName) {
return new MockElement(tagName);
},
Expand Down Expand Up @@ -122,7 +125,10 @@ test("the executable demo renders inert actions and blocks repeated status activ
document: {
title: "",
body: popupBody,
createElement(tagName) {
createDocumentFragment() {
return new MockDocumentFragment();
},
createElement(tagName) {
return new MockElement(tagName);
},
},
Expand Down
7 changes: 5 additions & 2 deletions src/test/js/dom-utils.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ 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) {
createDocumentFragment() {
return new MockDocumentFragment();
},
createElement(tagName) {
return new MockElement(tagName);
}
};
Expand Down
39 changes: 38 additions & 1 deletion src/test/js/mock-dom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ export class MockTextNode {
}
}

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 MockElement {
constructor(tagName = "div") {
this.tagName = tagName.toUpperCase();
Expand All @@ -31,12 +56,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