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
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake g++-13 ccache
sudo apt-get install -y cmake g++-13 ccache clang-format

- name: Restore ccache
uses: actions/cache@v4
Expand All @@ -44,6 +44,13 @@ jobs:
- name: Test floor gates (count floor + new-applet-must-have-tests)
run: bash tests/check_test_floor.sh

- name: Structure gates (banned-pattern + layering)
run: bash tests/check_structure_gates.sh

- name: clang-format dry-run (advisory — version skew until toolchain pinned)
continue-on-error: true
run: clang-format --dry-run --Werror $(find src include tests -name '*.cpp' -o -name '*.hpp')

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ C++23 现代 BusyBox 替代品(单二进制,123 applet,399 GTest,418 KB
- [document/ai/PLAN.md](document/ai/PLAN.md) — 当前焦点批级进度(批级,最易变)
- [document/ai/CODING-TASTE.md](document/ai/CODING-TASTE.md) — 编码/注释风格权威(写代码前读)
- [document/ai/COVERAGE.md](document/ai/COVERAGE.md) — 测试/正确性/差分标尺(覆盖率怎么算、怎么防退化,季级)
- [document/ai/STRUCTURE-TASTE.md](document/ai/STRUCTURE-TASTE.md) — 结构与工艺标尺(职责/DRY/边界/机械护栏,季级)
- [document/ai/PERFORMANCE.md](document/ai/PERFORMANCE.md) — 性能标尺(wall-clock 不动输出/4步闭环/Phase0 基建,季级)
- [document/notes/](document/notes/) — 批级工作记录(`<date>-<topic>.md`)

## 始终遵守(每条便宜,违规代价大)
Expand Down
24 changes: 24 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,28 @@ if(GTest_ADDED)
gtest_discover_tests(cfbox_tests)
endif()
endif()

# ── google-benchmark for perf micro-benchmarks (see document/ai/PERFORMANCE.md) ──
# Opt-in: -DCFBOX_ENABLE_BENCHMARK=ON with Release -O2. Doesn't fetch unless enabled.
option(CFBOX_ENABLE_BENCHMARK "Build perf micro-benchmarks (Release -O2)" OFF)
if(CFBOX_ENABLE_BENCHMARK)
CPMAddPackage(
NAME benchmark
GITHUB_REPOSITORY google/benchmark
GIT_TAG v1.9.0
OPTIONS "BENCHMARK_ENABLE_TESTING OFF"
)
if(benchmark_ADDED)
file(GLOB_RECURSE CFBOX_BENCH_SOURCES CONFIGURE_DEPENDS tests/benchmark/*.cpp)
if(CFBOX_BENCH_SOURCES)
add_executable(cfbox_bench ${CFBOX_BENCH_SOURCES} ${CFBOX_APPLET_SOURCES})
target_include_directories(cfbox_bench PUBLIC include)
target_include_directories(cfbox_bench PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/include)
target_link_libraries(cfbox_bench PRIVATE
cfbox_compiler_flags
benchmark::benchmark_main
)
endif()
endif()
endif()
endif()
2 changes: 2 additions & 0 deletions document/ai/DIRECTIVES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
- 注释一律英文;机械风格以 [.clang-format](../../.clang-format) 为准,跑 clang-format 不手调。
- Result:`auto v = CFBOX_TRY(expr)`(宏展开判 `!v` 后 `return std::unexpected`);成功 `return value;` / `Result<void>` 用 `return {};`;失败 `return std::unexpected(base::make_error(code, msg));`。
- 测试 / 覆盖率 / 差分(对照)正确性标尺见 [COVERAGE.md](COVERAGE.md):三种"覆盖率"语义、对照测试(标准答案)、测试数量只升不降。
- 结构与工艺标尺见 [STRUCTURE-TASTE.md](STRUCTURE-TASTE.md):职责/DRY/抽象边界/控制流纪律 + 机械护栏(禁裸 fopen、stoi/stol、不安全 C 函数;layering;clang-format 软门)。
- 性能标尺见 [PERFORMANCE.md](PERFORMANCE.md):只动 wall-clock 不动输出、4 步闭环、Phase-0 测量基建(google-benchmark via CPM)先行。

## C. 操作模型(长期,Claude 主力开发)

Expand Down
45 changes: 45 additions & 0 deletions document/ai/PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# CFBox 性能 标尺

> 这份文档讲性能优化的规矩——什么该优化、怎么测、怎么保证改完没改坏输出。大概一个季度才动一次。放在 `document/ai/`,从 [CLAUDE.md](../../CLAUDE.md) 和 [DIRECTIVES.md](DIRECTIVES.md) 指过来,和 [COVERAGE.md](COVERAGE.md)、[STRUCTURE-TASTE.md](STRUCTURE-TASTE.md) 平级。
>
> 一句话:性能优化的对象是"跑得快"(宿主 wall-clock),不是"算出不同的结果"——**cfbox 的输出字节 + 退出码在优化前后必须逐字节不变**,靠对照测试兜底(见 [COVERAGE.md](COVERAGE.md))。

## 一、核心立场(三条)

1. **只动 wall-clock,绝不动输出**。优化 sed/grep/sort/md5sum 这些命令的吞吐时,输出必须和优化前(以及和 BusyBox/GNU oracle)逐字节一致。任何"优化"若改变了输出,就是 bug。
2. **wall-clock 和 cycle-accuracy 是两根轴,不串**。CFBox 是宿主命令工具,没有 cycle-accurate 模拟器,只量真实 wall-clock;不要把"指令数"伪装成"用户体验"。
3. **依赖原则**:cfbox **二进制实现**零外部依赖(手写 deflate/POSIX regex,保持小体积);但**基建/测试用权威标准工具优先**——测试用 GTest、性能用 google-benchmark,都经 CPM 拉入(CPM 已经在用)。

## 二、每次性能改动走 4 步闭环

1. **构造真热路径场景**:拿真实大输入(大文件 grep/cat/sort、大目录 ls/find、大 tar、大 md5sum)来测,不写"空转微循环"那种白赢基准。
2. **对抗验保真**(两靶子):
- **场景保真**:基准真在跑那条热路径吗?没退化成"编译器一把梭"的常量循环吗?
- **测量可信**:方差/噪声多大?归因到对的代码了吗?编译器有没有把工作优化掉(看汇编/反汇编证伪)?
3. **确认 bench 提升 AND 正确性不变**:吞吐有可测提升,**且**对照 harness + GTest + 集成全绿。
4. **纳入防退化**:把这条热路径的基准固化进 bench 套件,以后回归自动盯。

## 三、测量基建(Phase 0,优先于一切优化)

现在**完全没有**性能基建,先建:

- **bench 框架 = google-benchmark(经 CPMAddPackage,参照现有 GTest 用法)**。做**进程内微基准**:cfbox 的 `*_main(int, char**)` 入口可在进程内直接调(单测已经在这么干),bench 复用这个接缝,量热路径算法本身(排除进程启动开销)。
- **构建档 = Release `-O2`**([CompilerFlag.cmake](../../cmake/compile/CompilerFlag.cmake) 已有,绝不 bench Debug/无优化构建)。
- **端到端 wall-clock**:另配一个 shell 脚本,对比 cfbox vs `/usr/bin/$cmd`(coreutils)在大样本上的耗时,贴近真实用户体验;复用对照 harness 的 fixture。
- **编译期 perf 计数器宏**:**暂缓**(在没有内测驱动前是死插桩;perf/callgrind 这种现成 profiler 对单二进制已能给源码级热点归因)。
- **防退化门 = advisory**(容差带 + 中位数,CI 只报告不挡):bench 噪声和 CI 机器方差大,hard 门会误报;稳定后再考虑。

## 四、热路径清单(hunting 已确认,按收益排,附录)

- **sed 每行重编译正则**([sed.cpp:229-262](../../src/applets/sed.cpp#L229-L262))— CPU-bound,high。修法:parse 阶段编译一次存进 SedCommand。
- **md5sum 整文件 2× 内存 + 非流式**([checksum.hpp:73-81](../../include/cfbox/checksum.hpp#L73-L81))— 大文件 high。修法:md5 加 update/finalize 增量 API。
- **tar -c 整归档缓存在一个 string**([tar.cpp:119-147](../../src/applets/tar.cpp#L119-L147))— high(条件)。修法:流式逐成员写。
- **cmp 双文件全量载入,首差异不早退**([cmp.cpp:33-50](../../src/applets/cmp.cpp#L33-L50))— medium。修法:双缓冲块读 + memcmp。
- **io::for_each_line 逐字符 fgetc**([io.hpp:137](../../include/cfbox/io.hpp#L137))— 实测约 10× 慢于块读,medium(公共骨架)。修法:块 fread + memchr 找换行。
- **head/tail/sort/uniq 先 read_all 再 split** — medium。修法:head 流式早退、tail 反向 seek。

## 五、执行批次(标尺确认后,每批 propose-then-execute,全 behavior-preserving)

- **批 0(基建)**:CPM 拉 google-benchmark + 加 `tests/benchmark/` 目标(Release -O2,进程内微基准)+ 端到端 wall-clock 脚本 + CI advisory 门。建立基线数字。
- **批 1(演示闭环)**:sed 每行重编译 → 修,bench 证提升 + 对照 oracle 证输出不变 + 进防退化。
- **批 2+**:md5sum 流式、tar 流式、cmp 早退、for_each_line 块读……每条都走 4 步闭环。
59 changes: 59 additions & 0 deletions document/ai/STRUCTURE-TASTE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# CFBox 结构与工艺 标尺

> 这份文档讲代码结构与工艺原则——怎么让代码"好读、好改、不容易在改动时引入 bug"。大概一个季度才动一次。放在 `document/ai/` 下,从 [CLAUDE.md](../../CLAUDE.md) 和 [DIRECTIVES.md](DIRECTIVES.md) 指过来,和 [CODING-TASTE.md](CODING-TASTE.md)(管命名/格式那种微观风格)、[COVERAGE.md](COVERAGE.md) 平级。
>
> 一句话:微观风格交给 clang-format 和 CODING-TASTE;这份管"函数多大、逻辑有没有重复、抽象边界有没有串"这种结构性的事。

## 一、七条结构与工艺原则(每条配 CFBox 真实例子)

1. **一个函数干一件事、停在一个抽象层次**。规模超限往往是职责堆叠的症状。
- 反例:tail.cpp(436 行)单文件混了"静态 tail + -f 跟随 + 信号处理"三件事;io::for_each_line 把"逐字符读"和"尾行边界处理"混在一层。
2. **DRY——逻辑唯一归属,第二份出现就抽**。
- 反例:用户名→名字解析(getpwuid 失败回退)在 ls/stat/id/whoami 写了 4 份,且回退策略不一致(stat 回退空串、其余回退数字);diff.cpp 的 hunk 行号统计整段复制两遍;sed.cpp 的分隔段抽取循环重复;sh_main 手写 fread 循环重复了 io::read_all。
3. **抽象边界不串——通用层不长出应用语义**。
- 反例:stream 层的 run_processor 自己拼 "cfbox:" 错误信息(应用语义泄漏进通用流层);fs::for_each_entry 静默吞掉迭代错误。
4. **显式优先 / 数据驱动 dispatch——优先级是语义时做成有序表,别藏进 if-else 物理顺序**。
- ✅ 正面:APPLET_REGISTRY 是 constexpr 数据表(分发靠查表,不是 if-else 链);find/test 用递归下降解析器,优先级做进语法结构。
5. **可测试性是设计约束——留可独立测试的接缝**。
- ✅ 正面:test_capture.hpp 在进程内直接调 `_main`,等于一个轻量 harness;反面:LineProcessor 这个虚基类被引入却零引用(死抽象)。
6. **最简表达——别写"算恒等"的冗余代码**。
- 反例:io::for_each_line 尾部两个 constexpr 分支体逐字相同;ErrorView 类型只在单测里用、生产零引用(YAGNI 死代码)。
7. **控制流纪律——嵌套≤3 层;一行 if 也用 {}**。
- 反例:个别函数嵌套 ≥3 层(diff 的 build_hunks)。整体偏干净。

## 二、机械护栏(脚本可查、版本无关的进 CI 当硬门;版本敏感的做软门)

**硬门(CI exit 非零,精确匹配函数调用、不匹配注释/标识符):**

- **禁不安全 C 函数**:`sprintf(`/`strcpy(`/`strcat(`/`gets(` → 用 `snprintf` / `std::string` / `std::string_view` 替代。现状:0 处真违规(历史 grep 命中全是注释/awk 函数名误报)。
- **禁裸 `std::fopen`(在 applets 里)**:文本/二进制读写走 `cfbox::io::open_file`(带 Result + RAII)。现状:15 处,本维度清掉。
- **禁 `std::stoi`/`std::stol`**:项目 -fno-exceptions,这俩在非法输入上抛异常→std::terminate(崩溃)。改 `std::strtol`+errno 或 `std::from_chars` + CFBOX_ERR 干净报错。现状:19 处(全是命令行数值参数解析),本维度清掉。
- **layering 门**:applets 不自造递归目录遍历,用 `cfbox::fs::for_each_entry` 或加注释豁免。现状:5 处,grandfather 进基线清单,新加的裸奔才挡。

**软门(advisory,本地 + IDE;不上 CI Werror,直到 CI pin 死工具链版本):**

- **clang-format dry-run**:CI 加一步检查(先只报告;CODING-TASTE 说"CI 也跑",现状没跑——补上)。
- **clang-tidy**(含 `readability-function-size` 行数门、`readability-braces` 等):版本敏感,advisory。

## 三、每次改结构,走这三步

1. 先看牵连:grep 被改抽象的引用方(`cfbox::fs::`、`cfbox::io::`、被改头文件的 include 方)。
2. 行为不变:结构改动不得改变可观测输出——靠 GTest + 集成 + 对照测试兜底(见 [COVERAGE.md](COVERAGE.md))。
3. 收尾同步:改完跑 clang-format;结构性决策记进 [document/notes/](../notes/)。

## 四、附录:CFBox 现状(2026-06-28 勘查)

- **死抽象**:`stream::LineProcessor`/`run_processor`([stream.hpp:57-78](../../include/cfbox/stream.hpp#L57-L78),0 引用)、`base::ErrorView`([error.hpp:14-17](../../include/cfbox/error.hpp#L14-L17),生产零引用)。
- **DRY 债**:owner 名字解析 4 份回退不一致;diff hunk 统计复制两遍;sed 段抽取重复;sh_main 重复 read_all。
- **banned-pattern**:裸 fopen 15 处、stoi/stol 19 处、不安全 C 函数 0 处真违规。
- **layering**:5 个 applet 自造递归遍历(grep/find/tar/du/sysctl)。
- **微观风格门缺失**:CI 没 clang-format/clang-tidy 步骤。
- **大文件**(职责堆叠候选,单文件):tail.cpp 436、sed.cpp 368、ls.cpp 336、find.cpp 313。(sh/awk 是多文件按职责拆分,大但合理。)

## 五、执行批次(标尺确认后,每批 propose-then-execute,全行为保持)

- **批1(零风险)**:删死抽象 LineProcessor/run_processor + ErrorView(+ 其单测)。0 引用,体积小降。
- **批2**:清 19 处 stoi/stol → strtol/from_chars + CFBOX_ERR(修潜在崩溃)。
- **批3**:清 15 处裸 fopen → io::open_file / read_all / for_each_line。
- **批4**:DRY 收敛——owner 名字解析(统一回退)、diff hunk、sed 段抽取。
- **批5**:上机械护栏脚本(banned-pattern 精确匹配 + layering grandfather)+ clang-format dry-run,接进 CI。
23 changes: 23 additions & 0 deletions include/cfbox/args.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
#pragma once

#include <cerrno>
#include <climits>
#include <cstdlib>
#include <initializer_list>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <cfbox/error.hpp>

namespace cfbox::args {

struct OptSpec {
Expand Down Expand Up @@ -156,4 +162,21 @@ inline auto parse(int argc, char* argv[],
return result;
}

// Parse s as a base-10 int with no throw (the project is -fno-exceptions, so std::stoi
// would std::terminate on bad input). Rejects empty / non-numeric / trailing junk /
// out-of-int-range; callers report via CFBOX_ERR on the unexpected path.
// Uses strtol (<cstdlib>) rather than from_chars (<charconv>): args.hpp is included
// by every applet, and <charconv> is a notoriously heavy header that blew up
// per-TU compile memory enough to OOM the parallel cross-compile builds.
[[nodiscard]] inline auto parse_int(std::string_view s) -> base::Result<int> {
std::string tmp{s};
char* end = nullptr;
errno = 0;
long v = std::strtol(tmp.c_str(), &end, 10);
if (errno != 0 || end == tmp.c_str() || *end != '\0' || v < INT_MIN || v > INT_MAX) {
return std::unexpected(base::Error{EINVAL, "not a valid integer: '" + std::string{s} + '\''});
}
return static_cast<int>(v);
}

} // namespace cfbox::args
Loading
Loading