diff --git a/.jules/bolt.md b/.jules/bolt.md index b08b203a..11335d74 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -4,3 +4,6 @@ ## 2026-07-12 - Optimize renderTaskRow DOM allocations **Learning:** Caching unattached template nodes and instantiating them via `.cloneNode(false)` reduces DOM instantiation overhead in O(N) render loops significantly. **Action:** Apply this optimization to other hot-path rendering elements such as rows, cells, and stack containers. +## 2026-08-21 - 핫 패스(hot paths)에서의 문자열 패딩 오버헤드 +**Learning:** 이 날짜 포맷팅 루프를 대상으로 한 Node/V8 마이크로벤치마크에서는 `String.padStart(2, '0')`를 제거한 구현의 실행 시간이 더 짧았습니다. 이 결과는 런타임/JIT에 종속적이며, 보편적인 GC 비용이나 JS-C++ 경계 전환 메커니즘을 입증하는 근거로 확장하지 않습니다. +**Action:** 현재 포맷터에는 출력 의미와 `padStart` 비사용을 고정하는 결정적 회귀 테스트를 유지하고, 다른 경로나 브라우저 런타임으로 최적화를 확대하기 전에는 해당 실행 환경에서 다시 측정합니다. diff --git a/app.js b/app.js index a04aae71..a00cbf1b 100644 --- a/app.js +++ b/app.js @@ -2684,20 +2684,28 @@ function clamp(value, min, max) { function formatDateInput(date) { const year = date.getUTCFullYear(); - const month = String(date.getUTCMonth() + 1).padStart(2, '0'); - const day = String(date.getUTCDate()).padStart(2, '0'); + const m = date.getUTCMonth() + 1; + const month = m < 10 ? '0' + m : m; + const d = date.getUTCDate(); + const day = d < 10 ? '0' + d : d; return `${year}-${month}-${day}`; } function formatLocalDateInput(date) { const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const m = date.getMonth() + 1; + const month = m < 10 ? '0' + m : m; + const d = date.getDate(); + const day = d < 10 ? '0' + d : d; return `${year}-${month}-${day}`; } function formatCompactDate(date) { - return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`; + const m = date.getMonth() + 1; + const month = m < 10 ? '0' + m : m; + const d = date.getDate(); + const day = d < 10 ? '0' + d : d; + return `${date.getFullYear()}${month}${day}`; } function formatPercent(value, digits) { diff --git a/package.json b/package.json index 8cefdc74..8c911a94 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "coverage": "npm run test:coverage", "server": "node server/server.mjs", "test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs", - "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs", + "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/date-formatting-performance.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs", "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", - "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", + "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/date-formatting-performance.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", "test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js tests/e2e/toast-accessibility.spec.js", diff --git a/tests/unit/date-formatting-performance.test.mjs b/tests/unit/date-formatting-performance.test.mjs new file mode 100644 index 00000000..f98fc0cf --- /dev/null +++ b/tests/unit/date-formatting-performance.test.mjs @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; + +const APP_PATH = new URL('../../app.js', import.meta.url); + +function loadDateFormatters() { + const source = fs.readFileSync(APP_PATH, 'utf8'); + const start = source.indexOf('function formatDateInput(date) {'); + const end = source.indexOf('\nfunction formatPercent(value, digits) {', start); + + assert.notEqual(start, -1, 'formatDateInput must remain present in app.js'); + assert.notEqual(end, -1, 'date formatter block must remain bounded by formatPercent'); + + return new Function( + `${source.slice(start, end)}\nreturn { formatDateInput, formatLocalDateInput, formatCompactDate };` + )(); +} + +test('date formatters avoid padStart while preserving zero-padded output', () => { + const { formatDateInput, formatLocalDateInput, formatCompactDate } = loadDateFormatters(); + const originalPadStart = String.prototype.padStart; + String.prototype.padStart = function forbiddenPadStart() { + throw new Error('date formatting regressed to String.prototype.padStart'); + }; + + try { + assert.equal(formatDateInput(new Date(Date.UTC(2026, 0, 2, 15, 4, 5))), '2026-01-02'); + assert.equal(formatDateInput(new Date(Date.UTC(2026, 10, 12, 15, 4, 5))), '2026-11-12'); + + const singleDigitLocal = new Date(2026, 0, 2, 12, 0, 0); + assert.equal(formatLocalDateInput(singleDigitLocal), '2026-01-02'); + assert.equal(formatCompactDate(singleDigitLocal), '20260102'); + + const doubleDigitLocal = new Date(2026, 10, 12, 12, 0, 0); + assert.equal(formatLocalDateInput(doubleDigitLocal), '2026-11-12'); + assert.equal(formatCompactDate(doubleDigitLocal), '20261112'); + } finally { + String.prototype.padStart = originalPadStart; + } +});