XPerf
语言Zig

`zperf-lint`

这篇还没有中文,下面是英文原文。

Performance and memory anti-pattern checker for Zig 0.17.0. Zig + std.zig.Ast, one file at a time, no types. Findings are review prompts, not compiler errors.

Licensed under the MIT License.

Build

Requires Zig 0.17.0 and has no third-party dependencies.

zig build -Doptimize=fast
zig build test --summary all
zig build run -- ../examples

The executable is written to zig-out/bin/zperf-lint.

Usage

zperf-lint [options] <file-or-directory>...

  -f, --format <text|json>       Output format (default: text)
  --enable <rules,...>           Enable only these rules (IDs or names)
  --disable <rules,...>          Disable rules
  --large-struct-bytes <N>       zp-large-struct-by-value threshold (default: 64)
  --hot-fn <names,...>           Add hot function names for zp-per-packet-alloc; a trailing * is a prefix wildcard
  --hot-fn-preset <name>         Append a named hot-fn set (`server`: accept/handleRequest/serve/…)
  --no-hot-propagation           Do not propagate hot status through local calls
  --strict-log-in-loop           zp-print-hot also flags `std.log.*` / `log.*` in loops (default: `debug.print` only)
  --include-tests                Also lint `build.zig`, `**/tests/**`, `**/*test*.zig`, `**/testing/**`, `**/Build/**`, `**/*benchmark*.zig`, `**/benchmarks/**`
  --include-scripts              Also lint files under `scripts/` (skipped by default)
  --include-compiler-rt          Also lint `compiler_rt` / `lib/compiler_rt` (skipped by default)
  -j, --jobs <N>                 Worker count (default: CPU count)
  --profile                      Print cumulative phase timings to stderr
  --no-fail                      Return 0 even when findings or parse errors exist (JSON includes `parse_errors`)
  --list-rules                   List rules, fixes, and false-positive risks
  -h, --help

Directories are traversed recursively for .zig files. .zig-cache and zig-out are skipped. Default path skips (opt-in flags in parentheses):

SkipFlag to include
build.zig, t.zig, **/tests/**, **/testing/**, *test*.zig--include-tests
**/Build/**, Build.zig (std.Build graph)--include-tests
*benchmark*.zig, **/benchmarks/**--include-tests
*fuzz*.zig, **/fuzz/**--include-tests
scripts/--include-scripts
compiler_rt/, lib/compiler_rt/, compiler_rt.zig--include-compiler-rt

Rule names and IDs both work, for example --enable=zp-shift-on-read,page-allocator-small. Output order is stable across worker counts.

Exit codes: 0 means no findings (or --no-fail); 1 means findings; 2 means invalid arguments, I/O errors, or source parse errors. With --no-fail, parse errors are still reported (stderr + JSON parse_errors) but do not force exit 2, so other files' findings remain usable.

Tip Zig / parse resilience

Zig tip syntax errdefer |err| expr is not accepted by std.zig.Ast on 0.17.0. When the first parse fails, zperf-lint rewrites payload captures to a 0.17-parseable block (errdefer { const err: anyerror = undefined; _ = expr; }) and retries — similar to the Zig 0.16 ** compatibility path. Files that still fail to parse are listed in JSON parse_errors; with --no-fail the run exits 0. Column positions in rewritten files may shift slightly after the insert.

Zig 0.16 compatibility

The Zig 0.17 tokenizer removed ** array repetition. If parsing fails, the checker retries after replacing ** outside strings and comments with same-length * sequences. This preserves byte offsets and line/column locations and prints a compatibility notice to stderr. The compatibility fixture is fixtures/compat016.zig.

Text output

The message line carries the Chinese message, then the English one. The 建议: line (suggested fix) and the summary line are Chinese.

fixtures/bad/zp-shift-on-read.zig:8:29: warning[zp-shift-on-read shift-on-read] 读走前缀后把剩余数据整体搬回开头 / remaining bytes shifted back to the front after consuming a prefix: std.mem.copyForwards(self.ready.items[0..], self.ready.items[k..])
    建议: 若每次读取后立即搬移,小块读取时总成本可达 O(n²);改用读游标,缓冲区耗尽时清零,或仅在已消费至少一半/追加空间不足时压缩,以保证搬移被后续读取摊销;持续流量下也可用环形缓冲/std.fifo

共检查 1 个文件,发现 1 处(warning 1, info 0, hint 0)

JSON output

The tool prints one line; it is wrapped here for reading.

{
  "tool": "zperf-lint",
  "files": 1,
  "count": 1,
  "findings": [
    {
      "file": "fixtures/bad/zp-shift-on-read.zig",
      "line": 8,
      "column": 29,
      "rule": "zp-shift-on-read",
      "name": "shift-on-read",
      "severity": "warning",
      "message_zh": "读走前缀后把剩余数据整体搬回开头",
      "message_en": "remaining bytes shifted back to the front after consuming a prefix",
      "detail": "std.mem.copyForwards(self.ready.items[0..], self.ready.items[k..])",
      "fix": "若每次读取后立即搬移,小块读取时总成本可达 O(n²);改用读游标,缓冲区耗尽时清零,或仅在已消费至少一半/追加空间不足时压缩,以保证搬移被后续读取摊销;持续流量下也可用环形缓冲/std.fifo"
    }
  ],
  "parse_errors": []
}

The message_zh and fix fields are Chinese; documentation and source comments are maintained in English.

Suppressions

// zperf-lint: ignore zp-alloc-hot       // Ignore this line and the next line.
const buf = try gpa.alloc(u8, n); // zperf-lint: ignore alloc-in-loop
// zperf-lint: ignore             // Ignore all rules on this line and the next.
// zperf-lint: ignore-file zp-packable-bools,zp-usize-index-field
// zperf-lint: ignore zp-print-hot -- Intentional diagnostic output.

Rules

Severity levels are warning (likely problem), info (review recommended), hint (usually relevant only on a hot path), and off. The severity column is the default from src/rules.zig.

off rules were demoted after G3/r4 triage on zig std/compiler, tigerbeetle, and ghostty (OK-idiom mass). As built, the command line never reports an off rule: --enable only selects among the active rules, and only the fixture tests turn off rules on (include_off_rules in src/lint.zig). The nine default-on rules are zp-page-allocator-small, zp-broad-runtime-safety, zp-static-string-map, zp-queue-ordered-remove, zp-shift-on-read, zp-invariant-float-div, zp-arena-reset-free-all, zp-create-destroy-hot, zp-fixed-size-alloc-hot. Options that tune an off rule (--large-struct-bytes, --strict-log-in-loop, --hot-fn, --hot-fn-preset) therefore have no visible effect on the command line.

IDNameSeverityDetection
zp-alloc-hotalloc-in-loopoffHeap allocation in a loop body; skips arena-like receivers and soft-arena args (sema.arena, *.arena, same as zp-append-without-reserve).
zp-append-without-reserveappend-without-reserveoffAppend in a loop without prior capacity reservation; skips arena/alloc args and test blocks.
zp-page-allocator-smallpage-allocator-smallwarningDirect use of page_allocator / *.page_allocator for small or container allocations (not huge_page_allocator).
zp-debug-allocatordebug-allocatoroffGeneralPurposeAllocator/DebugAllocator use without a visible build-mode selection.
zp-large-struct-by-valuelarge-struct-by-valueoffPassing a large locally declared struct by value.
zp-print-hotprint-in-loopoffstd.debug.print inside a loop; std.log.* only with --strict-log-in-loop.
zp-unbuffered-iounbuffered-iooffEmpty-buffer reader/writer; skips tests, readVec/writeVec, same-line buffer comments, bulk helpers, one-shot writes.
zp-missing-freemissing-freeoffAllocation or initialization without visible deinit/free/destroy or ownership transfer.
zp-broad-runtime-safetybroad-runtime-safetywarning@setRuntimeSafety(false) covers a broad scope.
zp-packable-boolspackable-boolsoffSeveral bool fields that may be packed or stored out of band.
zp-usize-index-fieldusize-index-fieldoffAn index-like field uses usize.
zp-aos-single-fieldaos-single-fieldoffA loop over an array of structs accesses only one field.
zp-catch-unreachable-alloccatch-unreachable-allocoffA fallible allocation is followed by catch unreachable.
zp-oversized-enum-tagoversized-enum-tagoffAn explicitly declared enum tag is wider than its value set needs.
zp-optional-scalar-fieldoptional-scalar-fieldoffAn optional scalar struct field may add padding.
zp-bool-arraybool-arrayoffA bool array or slice is used as a bit set.
zp-static-string-mapstatic-string-mapinfoA string hash map is populated only with literal keys.
zp-runtime-div-hotruntime-div-in-loopoffDivision or modulo by a runtime value inside a loop; skipped under crypto/ / compiler_rt/ paths and soft-div fn names (udiv*, …). Residual FPs: non-crypto number-theory / codec loops.
zp-queue-ordered-removequeue-ordered-removewarningorderedRemove is used as a queue operation or repeatedly in a loop.
zp-shift-on-readshift-on-readwarningRemaining bytes are shifted to the start after consuming a prefix.
zp-per-packet-allocper-packet-allocoffHeap allocation on an inferred or explicitly marked packet/frame hot path.
zp-invariant-float-divinvariant-float-divhintA loop-invariant floating-point divisor appears in an array-store kernel.
zp-map-put-without-ensuremap-put-without-ensureoffHashMap put/getOrPut in a loop without prior ensure*.
zp-catch-unreachable-iocatch-unreachable-iooffcatch unreachable on fallible I/O (not close/eventfd/kevent).
zp-arena-reset-free-allarena-reset-free-allwarningArenaAllocator.reset(.free_all) or arena deinit+recreate inside a loop; prefer retain_capacity / retain_with_limit.
zp-create-destroy-hotcreate-destroy-in-loopinfoallocator.create(T) paired with destroy in the same loop; prefer MemoryPool(T).
zp-fixed-size-alloc-hotfixed-size-alloc-in-loopinfoalloc(u8, comptime_int) (≥16) in a loop; prefer a size-class buffer pool.

Rules are syntax-based and conservative: no reliable types, ownership, ABI, bounds, or true hotness. See src/rules.zig and --list-rules for fixes and caveats.

Hot-path inference

zp-per-packet-alloc (currently off) recognizes common callback and processing names such as onPacket, receiveChunk, enqueueJob, and processFrame; callconv(.c) functions with bodies are also treated as possible entry points. Add // zperf-lint: hot immediately above a function or pass names with --hot-fn. Use --hot-fn-preset=server to append accept/handleRequest/serve/readLoop/… globs (see server_hot_preset in src/main.zig). Local calls propagate hot status by up to two levels; use --no-hot-propagation to disable that behavior. Diagnostic notes include the inference chain.

Tests and fixtures

fixtures/bad/<slug>.zig contains triggering examples; fixtures/good/<slug>.zig contains non-triggering examples. fixture_test.zig checks rule coverage, clean fixtures, suppressions, rule selection, and parse errors. Additional fixtures cover Zig 0.16 compatibility and hot-path propagation. Layout estimation also has unit tests in src/lint.zig.

Run zig build test --summary all to execute the test suite.