diff --git a/CMakeLists.txt b/CMakeLists.txt index 14e3b619..39a3699e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,10 @@ termforge_warnings(selection_test) add_executable(resize_repaint_witness EXCLUDE_FROM_ALL experiments/core/resize_repaint_witness.cpp) target_link_libraries(resize_repaint_witness PRIVATE termforge_core) termforge_warnings(resize_repaint_witness) +add_executable(history_memory_probe EXCLUDE_FROM_ALL experiments/core/history_memory_probe.cpp) +target_link_libraries(history_memory_probe PRIVATE termforge_core) +target_compile_definitions(history_memory_probe PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN) +termforge_warnings(history_memory_probe) add_executable(search_test experiments/core/search_test.cpp) target_link_libraries(search_test PRIVATE termforge_core) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 45dae3a7..7872a3fd 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -68,6 +68,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 范围 | 结果与复现入口 | 可以支持的结论与限制 | |---|---|---| | 行存储封存与结构单一发布(#169 第二片) | [固定结果](../experiments/core/results/2026-10-29-sealed-row-storage/README.md)、[验证身份](../experiments/core/results/2026-10-29-sealed-row-storage/validation.json) | `Screen::lines/history` 私有,只读口 + 23 文件/363 处迁移;Find overlay 改文档 rank;默认 Release 构建 exit 0、核心 8/8、扩展快测 20/20、candidate 源 /W4 编译 0。无新 VT/重排行为;运行中跨重排 Search、Qt 与候选运行由宿主作业复核。 | +| 保留历史的内存与单次 Search 成本(#164 剩余项) | [固定结果](../experiments/core/results/2026-10-29-history-memory/README.md)、[验证身份](../experiments/core/results/2026-10-29-history-memory/validation.json) | 生产 `Screen`/`TextSearch` 在 H=100/1000/10000、各 3 个新进程样本:每行私有增量收敛 ~5.8–6.9 KB,H=10000 私有提交 ~59 MB;整文档 Search ~54 ms/199 步。不是 r164 fork 对照,也不是性能预算;不关闭 #164。 | | 结构重排下的增量 Search(#169 延期项 / #27 前置) | [固定结果](../experiments/core/results/2026-10-29-search-reorder/README.md)、[验证身份](../experiments/core/results/2026-10-29-search-reorder/validation.json) | `TextSearch` 以文档 rank 维护扫描前缀:order revision 检测、局部 `repair_order`、rank 化 `rewind`/`match`/`prune`。新 `search_reorder_core` 覆盖扫描中重排、重排后导航、淘汰与有序回归;核心 10/10。无 UI/Find 集成、无重排成本测量,不声明 #27/R2 通过。 | | 默认核心缩小与重绘交错(#156) | [固定四-case机制诊断](../experiments/shell/results/2026-10-29-resize-repaint-witness/README.md)、[脱敏验证](../experiments/shell/results/2026-10-29-resize-repaint-witness/validation.json) | A/C保留2499B,B/D在第五段重绘首次失效且行分配路径不同;只见证机制,原CI切点/根因未知,未修生产。witness结束后Job仍active1,发起一次终止后归零;成员身份/逐句柄退出未知,不称全树自然退出。 | | 系统 ConPTY resize 后的客户端尺寸回复(#145 未关闭) | [有界重复采样](../experiments/conpty/results/2026-10-29-resize-reply-r145/README.md)、[固定验证身份](../experiments/conpty/results/2026-10-29-resize-reply-r145/validation.json) | 新增 tag 化探针:hosted 2000 次交替 resize + 500 次全新会话、本机 200 + 100,均无陈旧回复,buffer/window 无分歧,resize VT 全部先于回复,回复中位 8.7–16.1ms。只否定“resize 后立即查询存在必然竞态”;原两条 CI 失败、原始探针/5 秒等待与 #145/#38 状态不变。 | diff --git a/experiments/core/history_memory_probe.cpp b/experiments/core/history_memory_probe.cpp new file mode 100644 index 00000000..d45cca90 --- /dev/null +++ b/experiments/core/history_memory_probe.cpp @@ -0,0 +1,99 @@ +// Diagnostic only (issue #164 remainder): retained-history memory and a single +// bounded Search at fixed history sizes on the production Screen/TextSearch. +// It reports what it measured and asserts nothing about performance targets. +#include "screen.hpp" +#include "terminal.hpp" +#include "text_document.hpp" +#include "text_search.hpp" +#include +#include +#include +#include +#include +#include + +using namespace termforge; + +namespace { +struct Usage { SIZE_T working = 0, peak_working = 0, private_bytes = 0; }; + +Usage current_usage() { + using Fn = BOOL(WINAPI*)(HANDLE, PROCESS_MEMORY_COUNTERS_EX*, DWORD); + static const auto fn = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "K32GetProcessMemoryInfo")); + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (!fn || !fn(GetCurrentProcess(), &counters, sizeof(counters))) return {}; + return {counters.WorkingSetSize, counters.PeakWorkingSetSize, counters.PrivateUsage}; +} +long long micros(std::chrono::steady_clock::time_point from, std::chrono::steady_clock::time_point to) { + return std::chrono::duration_cast(to - from).count(); +} +} // namespace + +int main(int argc, char** argv) { + size_t history = 0, lines = 0; + std::string query = "needle"; + for (int index = 1; index < argc; ++index) { + const std::string arg = argv[index]; + if (arg == "--history" && index + 1 < argc) history = static_cast(std::stoul(argv[++index])); + else if (arg == "--lines" && index + 1 < argc) lines = static_cast(std::stoul(argv[++index])); + else if (arg == "--query" && index + 1 < argc) query = argv[++index]; + else { std::cerr << "usage: history_memory_probe --history N --lines M [--query text]\n"; return 2; } + } + if (!history || history > 10000 || !lines) { std::cerr << "history 1..10000 and lines>0 required\n"; return 2; } + + const Usage before = current_usage(); + Parser parser(24, 80, history); + // Fill the retained history: the live screen keeps 24 rows, the rest evict + // into the bounded history in document order. + const auto fill_begin = std::chrono::steady_clock::now(); + std::string batch; + batch.reserve(1 << 20); + for (size_t line = 0; line < lines; ++line) { + char prefix[16]; + std::snprintf(prefix, sizeof(prefix), "R%06zu ", line); + std::string text = prefix; + // One hit every 1000 rows, otherwise harmless filler. + if (line % 1000 == 0) text += query; + else text += "payload-"; + while (text.size() < 78) text += static_cast('a' + (line % 26)); + text += "\r\n"; + batch += text; + } + parser.feed(batch); + const auto fill_end = std::chrono::steady_clock::now(); + Sleep(150); // Let the allocator settle before the snapshot is taken. + const Usage after_fill = current_usage(); + + const TextDocument document(parser.screen); + TextSearch search; + const auto search_begin = std::chrono::steady_clock::now(); + const auto generation = search.start(document, query); + size_t steps = 0, work = 0; + while (search.status() == SearchStatus::running && steps < 1000000) { + const auto progress = search.step(document, generation, 4096); + work += progress.work_used; + ++steps; + } + const auto search_end = std::chrono::steady_clock::now(); + + std::cout << "{\"history_limit\":" << history + << ",\"lines_fed\":" << lines + << ",\"retained\":" << document.size() + << ",\"history_rows\":" << parser.screen.history_size() + << ",\"query\":\"" << query << "\"" + << ",\"search_status\":" << static_cast(search.status()) + << ",\"search_matches\":" << search.matches().size() + << ",\"search_steps\":" << steps + << ",\"search_work\":" << work + << ",\"fill_us\":" << micros(fill_begin, fill_end) + << ",\"search_us\":" << micros(search_begin, search_end) + << ",\"working_before\":" << before.working + << ",\"working_after\":" << after_fill.working + << ",\"private_before\":" << before.private_bytes + << ",\"private_after\":" << after_fill.private_bytes + << ",\"peak_working\":" << after_fill.peak_working + << "}" << std::endl; + return 0; +} diff --git a/experiments/core/results/2026-10-29-history-memory/README.md b/experiments/core/results/2026-10-29-history-memory/README.md new file mode 100644 index 00000000..bab8369f --- /dev/null +++ b/experiments/core/results/2026-10-29-history-memory/README.md @@ -0,0 +1,32 @@ +# 保留历史的内存与单次 Search 成本(#164 剩余项) + +#164 的最后一项要求"在相同正确性下记录定位/重建次数、内存与首访/搜索成本"覆盖 100/1000/10000 行。定位/重建已由 [line-order 成本](https://github.com/JavaZeroo/termforge/blob/main/experiments/core/results/2026-10-29-line-order-r164/README.md) 提供;本片补**生产 `Screen` 的历史内存**与**生产 `TextSearch` 在 H=10000 的整文档扫描成本**。 + +## 方法 + +新增 `history_memory_probe`(`EXCLUDE_FROM_ALL`,默认不构建、不注册 CTest): + +- 每个样本一个**新进程**,`Parser(24, 80, N)`,喂入 `N+40` 行(行宽 78 字节 + CRLF),每 1000 行含一次 `needle`; +- 用 `K32GetProcessMemoryInfo` 读工作集/私有提交/峰值,读点在填充前后各一次(中间 150ms 让分配器稳定); +- 用 `steady_clock` 记录填充与整文档 Search 的耗时、步数、work 单位与命中数; +- 每个 H 跑 3 个样本,原始 JSON 见 [validation.json](validation.json)。 + +## 结果(Windows 11 10.0.26200,Release,3 样本) + +| H | 保留行 | 命中 | Search 步数 | Search work | 填充 µs | Search µs | 私有提交 前→后 | 工作集 前→后 | 每行私有增量 | +|---|---|---|---|---|---|---|---|---|---| +| 100 | 124 | 0(第 0 行已淘汰) | 3 | 9,965 | 590–752 | 867–1,200 | 0.69→2.54 MB | 4.35→5.17 MB | ~14.5 KB | +| 1000 | 1024 | 1 | 21 | 82,865 | 5,120–7,839 | 5,916–6,023 | 0.67→7.69 MB | 4.34→10.33 MB | ~6.9 KB | +| 10000 | 10024 | 10 | 199 | 811,865 | 53,969–65,463 | 53,389–58,331 | 0.68→58.97 MB | 4.35→61.15 MB | ~5.8 KB | + +- 每行私有增量随 H 增大收敛到 **~5.8–6.9 KB**(H=100 含固定分配,故偏高);H=10000 时私有提交约 **59 MB**、工作集约 **61 MB**。 +- 整文档 Search 在 H=10000 为 **~54 ms**(199 步 × 4096 work),线性于行数;H=1000 为 ~6 ms。 +- 命中数符合预期:H=100 时第 0 行已淘汰、下一个命中在 1000 行且不在保留窗口内,故 0 命中;H=1000 保留 1 个;H=10000 保留 10 个。 + +## 解释与边界 + +- 这是**生产** `Screen`/`TextSearch` 的观测,不是 r164 fork 的 Scan/LazyTable 对照;两者不可直接比较。 +- 每行成本主要来自 `Cell` 的 `std::string text` 与完整属性结构(80 列 × 约 72 B),本轮未尝试压缩或共享存储;该数值是后续历史压缩/大历史实验的基线输入,**不是性能预算**。 +- 峰值工作集等于最终工作集(没有额外瞬时峰值),但进程基线含运行时与二进制映像;每行数字取自前后差值。 +- 未覆盖:淘汰/重排混合、分块填充、真实 ConPTY 输出、UI 呈现、长期增长;Search 只测一条查询与一个命中分布。 +- 本片不关闭 #164:H=10000 的静态对照配方(r164 fork)与 RSS 之外的项目仍按原记录分别成立。 diff --git a/experiments/core/results/2026-10-29-history-memory/validation.json b/experiments/core/results/2026-10-29-history-memory/validation.json new file mode 100644 index 00000000..56f5e67e --- /dev/null +++ b/experiments/core/results/2026-10-29-history-memory/validation.json @@ -0,0 +1,143 @@ +{ + "schema": "termforge-core-history-memory-v1", + "issue": 164, + "base_commit": "02c42e90114b535a9b5b88c566b504767fff8a61", + "configuration": { + "platform": "Windows-11-10.0.26200", + "generator": "Visual Studio 17 2022", + "build_type": "Release", + "binary": "history_memory_probe (EXCLUDE_FROM_ALL)" + }, + "method": { + "parser": "Parser(24, 80, N)", + "line_bytes": 80, + "hit_pattern": "one needle every 1000 rows", + "samples_per_size": 3, + "process": "fresh process per sample", + "memory_api": "K32GetProcessMemoryInfo" + }, + "samples": [ + { + "history": 100, + "retained": 124, + "matches": 0, + "steps": 3, + "work": 9965, + "fill_us": [ + 590, + 581, + 752 + ], + "search_us": [ + 987, + 867, + 1200 + ], + "private_before": [ + 741376, + 684032, + 688128 + ], + "private_after": [ + 2539520, + 2568192, + 2572288 + ], + "working_before": [ + 4837376, + 4349952, + 4354048 + ], + "working_after": [ + 5652480, + 5169152, + 5173248 + ] + }, + { + "history": 1000, + "retained": 1024, + "matches": 1, + "steps": 21, + "work": 82865, + "fill_us": [ + 5719, + 5120, + 7839 + ], + "search_us": [ + 6023, + 6006, + 5916 + ], + "private_before": [ + 671744, + 684032, + 684032 + ], + "private_after": [ + 7688192, + 7692288, + 7692288 + ], + "working_before": [ + 4337664, + 4345856, + 4349952 + ], + "working_after": [ + 10330112, + 10338304, + 10395648 + ] + }, + { + "history": 10000, + "retained": 10024, + "matches": 10, + "steps": 199, + "work": 811865, + "fill_us": [ + 65463, + 53969, + 55176 + ], + "search_us": [ + 58331, + 53796, + 53389 + ], + "private_before": [ + 675840, + 679936, + 679936 + ], + "private_after": [ + 58966016, + 58830848, + 58970112 + ], + "working_before": [ + 4341760, + 4345856, + 4345856 + ], + "working_after": [ + 61145088, + 61120512, + 61198336 + ] + } + ], + "source_sha256": { + "experiments/core/history_memory_probe.cpp": "787b4eaa7f8b13b27cc2429cf9f79412d60315fcb1ed3993388866b40e0ffba5", + "CMakeLists.txt": "ada254edee850f8570973ffa499ea64532507bf2a510e109e079a7231f45dc6b" + }, + "limitations": [ + "生产 Screen/TextSearch,不是 r164 fork 的 Scan/LazyTable 对照", + "未覆盖淘汰/重排混合、真实 ConPTY、UI 与长期增长", + "单查询单命中分布;不是性能预算或容差", + "进程基线含运行时与二进制映像" + ], + "performance_claim": false +}