From b58073730fb3b00be76528a36701c747d5a44d78 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 27 Sep 2026 20:25:30 +0900 Subject: [PATCH] =?UTF-8?q?test(spreadsheet):=20A=20=EC=97=94=EC=A7=84=20?= =?UTF-8?q?=EB=AC=B4=EA=B2=8C=20=EC=8B=9C=ED=97=98=20=E2=80=94=20=EC=B9=B8?= =?UTF-8?q?=201=EB=A7=8C=20=C2=B7=20=EC=8B=9D=205=EC=B2=9C=20=EC=9E=AC?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=C2=B7=206=EB=A7=8C=208=EC=B2=9C=20?= =?UTF-8?q?=EC=B9=B8=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=EB=82=98=EB=88=97=EC=85=88=20=EC=82=AC=EC=8A=AC=20=EB=B6=84?= =?UTF-8?q?=EB=AA=A8=20=EC=83=81=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bench_spreadsheet_engine.cjs — 얕은 시트 · 깊은 사슬 5천 · 큰 시트(271 행 × 253 열) · 소수 역수 합 · 곱나눗셈 사슬 수치 - test_spreadsheet_bench.py — 딸린 칸 수 · 시간 한도(실측 몇 배) · 분모 41 자리 안 · double 과 상대 오차 1e-12 안 - helper_spreadsheet_engine.cjs — 적재 · 가짜 함수를 무게 시험이 같이 씀 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF --- .../spreadsheet/bench_spreadsheet_engine.cjs | 113 ++++++++++++++++++ .../spreadsheet/helper_spreadsheet_engine.cjs | 4 + .../spreadsheet/test_spreadsheet_bench.py | 54 +++++++++ 3 files changed, 171 insertions(+) create mode 100644 resources/tester/spreadsheet/bench_spreadsheet_engine.cjs create mode 100644 resources/tester/spreadsheet/test_spreadsheet_bench.py diff --git a/resources/tester/spreadsheet/bench_spreadsheet_engine.cjs b/resources/tester/spreadsheet/bench_spreadsheet_engine.cjs new file mode 100644 index 00000000..72175187 --- /dev/null +++ b/resources/tester/spreadsheet/bench_spreadsheet_engine.cjs @@ -0,0 +1,113 @@ +/* 스프레드시트 엔진 A 무게 시험 — 큰 시트 불러오기 · 칸 하나 고칠 때 재계산 · 나눗셈 사슬 분모. + * 수치를 JSON 으로 냄 → `test_spreadsheet_bench.py` 가 한도 안인지 봄. 적재 · 가짜 함수는 엔진 시험 헬퍼 것. + */ +const { A, C, E, F, G } = require("./helper_spreadsheet_engine.cjs"); + +const ms = (fn) => { + const t = process.hrtime.bigint(); + const out = fn(); + return [Number(process.hrtime.bigint() - t) / 1e6, out]; +}; +const round = (x) => Math.round(x * 100) / 100; + +/** 칸 표 → 시트 하나 통합문서 JSON 글(불러오기 = JSON.parse + 엔진 만들기) */ +function bookText(cells) { + const 칸 = {}; + for (const [k, v] of Object.entries(cells)) 칸[k] = typeof v === "string" ? { 식: v } : { 값: v }; + return JSON.stringify({ 종류: "통합문서", 판: 1, 열: "b", 서식: [{}], 시트: [{ id: "s1", 이름: "무게", 칸 }] }); +} + +/** 불러오기 · 칸 하나 고치기(가운데 · 맨 앞) 시간 */ +function scenario(cells, edits) { + const text = bookText(cells); + const [parseMs, book] = ms(() => JSON.parse(text)); + const [loadMs, engine] = ms(() => G.createCalcEngine(book)); + const out = { cells: Object.keys(cells).length, kb: Math.round(text.length / 1024), parseMs: round(parseMs), loadMs: round(loadMs) }; + for (const [label, a1, value] of edits) { + const times = []; + let redone = 0; + for (let i = 0; i < 5; i++) { + const fx = C.applyCommand(book, { 종류: "칸", 시트: "s1", 칸: { [a1]: { 값: value + i } } }); + const [t, list] = ms(() => engine.update(fx.cells)); + times.push(t); + redone = list.length; + } + times.sort((x, y) => x - y); + out[label] = { ms: round(times[2]), redone }; + } + return [out, engine]; +} + +const N = 5000; +const result = {}; + +// ① 얕은 시트 — 칸 1만(값 5천 · 식 5천) · 식마다 이웃 두 칸 · 끝에 합계 하나 +{ + const cells = {}; + for (let i = 1; i <= N; i++) cells[`A${i}`] = (i % 97) / 10; + for (let i = 1; i < N; i++) cells[`B${i}`] = `ROUNDDOWN((A${i}+A${i + 1}/2)*1.3,2)`; + cells[`B${N}`] = `SUM(B1:B${N - 1})`; + const [out, engine] = scenario(cells, [ + ["editMiddle", "A2500", 3], + ["editFirst", "A1", 3], + ]); + out.total = E.toCalcValue(engine.value("s1", N - 1, 1)); + result.shallow = out; +} + +// ② 깊은 사슬 — 칸 1만 · 식 5천이 한 줄로 이어짐(맨 앞 고치면 5천 칸 모두 다시) +{ + const cells = {}; + for (let i = 1; i <= N; i++) cells[`A${i}`] = 1; + cells.B1 = "A1"; + for (let i = 2; i <= N; i++) cells[`B${i}`] = `B${i - 1}+A${i}*1.1`; + const [out, engine] = scenario(cells, [ + ["editLast", `A${N}`, 2], + ["editFirst", "A1", 2], + ]); + out.end = E.toCalcValue(engine.value("s1", N - 1, 1)); + result.chain = out; +} + +// ③ 큰 시트 불러오기 — 실무 최대 모양(271 행 × 253 열) 에 가까운 6만 8천 칸 · 절반이 식 +{ + const cells = {}; + for (let r = 1; r <= 271; r++) + for (let c = 0; c < 253; c++) { + const a1 = A.toA1(r - 1, c); + cells[a1] = c % 2 === 0 ? r + c / 100 : `${A.toA1(r - 1, c - 1)}*2+${A.toA1(Math.max(0, r - 2), c - 1)}/3`; + } + const [out] = scenario(cells, [["editOne", "A1", 5]]); + result.bigSheet = out; +} + +// ④ 나눗셈 사슬 — 분모가 커지는 식(1/소수 더하기 · 소수로 나누기) · 상한(10^40) 넘으면 30 자리 십진으로 굳힘 +{ + const primes = []; + for (let n = 2; primes.length < 2000; n++) if (primes.every((p) => n % p)) primes.push(n); + const cells = { A1: 1, C1: 1 }; + for (let i = 2; i <= 2000; i++) { + cells[`A${i}`] = `A${i - 1}+1/${primes[i - 1]}`; // 역수 합 — 분모가 소수 곱으로 부풂 + cells[`C${i}`] = `C${i - 1}/${primes[(i * 7) % 2000]}*${primes[(i * 13) % 2000]}`; // 곱 · 나눗셈 사슬 + } + const book = JSON.parse(bookText(cells)); + const [loadMs, engine] = ms(() => G.createCalcEngine(book)); + let maxDigits = 0; + for (let i = 1; i <= 2000; i++) + for (const c of [0, 2]) { + const v = engine.value("s1", i - 1, c); + maxDigits = Math.max(maxDigits, v.d.toString().length); + } + let dbl = 1; + for (let i = 2; i <= 2000; i++) dbl += 1 / primes[i - 1]; + const exact = Number(F.fracToString(engine.value("s1", 1999, 0))); + result.divChain = { + cells: 4000, + loadMs: round(loadMs), + maxDenominatorDigits: maxDigits, + sum: F.fracToString(engine.value("s1", 1999, 0)), + relErrVsDouble: Math.abs(exact - dbl) / dbl, + }; +} + +process.stdout.write(JSON.stringify(result)); diff --git a/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs b/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs index 3abde1ea..0747dd8a 100644 --- a/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs +++ b/resources/tester/spreadsheet/helper_spreadsheet_engine.cjs @@ -98,6 +98,10 @@ const C = load("spreadsheet_commands"); const H = load("spreadsheet_history"); const RC = load("spreadsheet_recalc"); +// 무게 시험(`bench_spreadsheet_engine.cjs`)이 같은 적재 · 가짜 함수를 씀 — 불려 올 때는 시험을 안 돌림 +module.exports = { A, C, E, F, G }; +if (require.main !== module) return; + const results = {}; let group = ""; const fails = () => (results[group] ??= []); diff --git a/resources/tester/spreadsheet/test_spreadsheet_bench.py b/resources/tester/spreadsheet/test_spreadsheet_bench.py new file mode 100644 index 00000000..130bed31 --- /dev/null +++ b/resources/tester/spreadsheet/test_spreadsheet_bench.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +"""산출근거 스프레드시트 엔진 A 무게 시험 — 칸 1만 · 식 5천 시트에서 칸 하나 고칠 때 재계산 · +6만 8천 칸 불러오기 · 분모가 커지는 나눗셈 사슬(상한 10^40 넘으면 30 자리 십진으로 굳힘). + +Node 헬퍼(`bench_spreadsheet_engine.cjs`)가 수치를 냄. 한도는 이 PC 실측의 몇 배로 넉넉히 둠 — +실측이 한도 가까이 오르면 느려진 것. +""" + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture(scope="module") +def bench(): + proc = subprocess.run( + ["node", os.path.join(HERE, "bench_spreadsheet_engine.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=300, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_얕은_시트_칸_하나는_딸린_칸만(bench): + s = bench["shallow"] + assert s["editMiddle"]["redone"] == 4 # 고친 칸 + 이웃 식 둘 + 합계 + assert s["editMiddle"]["ms"] < 100 + assert s["loadMs"] < 3000 + + +def test_깊은_사슬_5천_칸(bench): + c = bench["chain"] + assert c["editFirst"]["redone"] == 5001 + assert c["editFirst"]["ms"] < 1000 + assert c["editLast"]["redone"] == 2 + + +def test_큰_시트_불러오기(bench): + b = bench["bigSheet"] + assert b["cells"] > 60000 + assert b["parseMs"] + b["loadMs"] < 15000 + + +def test_나눗셈_사슬_분모_상한(bench): + d = bench["divChain"] + assert d["maxDenominatorDigits"] <= 41 + assert d["relErrVsDouble"] < 1e-12