Skip to content
Merged
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 CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/EVIDENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 状态不变。 |
Expand Down
99 changes: 99 additions & 0 deletions experiments/core/history_memory_probe.cpp
Original file line number Diff line number Diff line change
@@ -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 <windows.h>
#include <psapi.h>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <string>

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<Fn>(
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<std::chrono::microseconds>(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<size_t>(std::stoul(argv[++index]));
else if (arg == "--lines" && index + 1 < argc) lines = static_cast<size_t>(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<char>('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<int>(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;
}
32 changes: 32 additions & 0 deletions experiments/core/results/2026-10-29-history-memory/README.md
Original file line number Diff line number Diff line change
@@ -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),本轮未尝试压缩或共享存储;该数值是后续历史压缩/大历史实验的基线输入,**不是性能预算**。
- 峰值工作集等于最终工作集(没有额外瞬时峰值),但进程基线含运行时与二进制映像;每行数字取自前后差值。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retain peak samples before claiming no transient memory peak

The statement that peak working set equals final working set cannot be checked against the published evidence: validation.json omits peak_working for every sample, although the probe emits it. Moreover, that peak is captured before TextSearch runs, so it cannot establish the absence of a transient peak across the whole experiment. Preserve the per-sample peak values and explicitly restrict this conclusion to the fill phase, or remove the unsupported conclusion. AGENTS.md requires raw samples for every performance claim.

AGENTS.md reference: AGENTS.md:L12-L12

Useful? React with 👍 / 👎.

- 未覆盖:淘汰/重排混合、分块填充、真实 ConPTY 输出、UI 呈现、长期增长;Search 只测一条查询与一个命中分布。
- 本片不关闭 #164:H=10000 的静态对照配方(r164 fork)与 RSS 之外的项目仍按原记录分别成立。
143 changes: 143 additions & 0 deletions experiments/core/results/2026-10-29-history-memory/validation.json
Original file line number Diff line number Diff line change
@@ -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
}