Visitar URL original
perf(lib): shrink the help, completion and error rendering code in usage-argv by jdx · Pull Request #1551 · jdx/usage · GitHub
Skip to content

perf(lib): shrink the help, completion and error rendering code in usage-argv - #1551

Merged
jdx merged 42 commits into
mainfrom
perf/argv-size
Oct 14, 2026
Merged

jdx merged 42 commits into
mainfrom
perf/argv-size

Conversation

@jdx

@jdx jdx commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

https://entire.io/gh/jdx/usage/trails/92

Binaries that embed usage-argv carry its help, completion and error rendering. In the oxlint and oxfmt migration to usage-rs, that was about +98 KB and +86 KB over their bpaf builds. This shrinks the code behind those paths without changing any output.

What changed

Almost all of it is the same few ideas applied across help.rs, help/template.rs, complete.rs, diagnostic.rs, lib.rs and spec.rs:

  • Appends go through shared out-of-line helpers (argv/src/text.rs). An inlined String::push_str costs about 100–130 bytes of code, and help text is built from hundreds of small appends. This was the largest single saving, about 12 KB.
  • Plain loops replace iterator adapter chains and closures in the flag walk, completion lists, help sections and usage lines. Each chain was monomorphised at every call site.
  • Characters are counted by one non-inlined routine instead of an expansion at each of 15 width calculations (about 8.6 KB).
  • Parse errors are written from templates with holes instead of per-site format!, and share one three-piece error line and one sigil-without-value error.
  • Flag spellings in help are compared as values (Form) rather than as formatted strings, and span tables live in statics.
  • The derive collects args_os once for both the completion intercept and the parse.

Help, -h, error, completion, spec and version output are unchanged byte for byte (plain, forced colour, and at width 200 on oxfmt's real CLI), and the whole conformance corpus passes. Several things that did not help were tried and left out: #[cold], #[inline(never)] on cold helpers, and replacing format! with &str arrays (that one grew the binary).

Measured

tasks/size.sh origin/main (stripped release builds):

binary base head change
usage CLI 11,470,512 11,415,760 -54,752 (-0.48%)
derived mise CLI, usage's share 941,584 930,640 -10,944 (-1.16%)

On the oxc PR's CLIs (cargo build --release -p oxlint -p oxfmt, linux x86_64, same machine and toolchain, usage-argv patched in from this checkout in both builds): main → this branch is oxlint 14,960,504 → 14,927,736 B (-32,768) and oxfmt 9,295,640 → 9,258,776 B (-36,864). That recovers roughly a third to nearly half of what usage adds to those binaries over their bpaf builds (+98,304 and +86,016 B). Absolute sizes from a path-patched build run about 24 KB above the crates.io build of the same source, so compare the deltas, not the absolutes.

Speed: interleaved, minimum-of-N timings of oxfmt startup (--help, -h, --version, a bad flag, __complete_word__) show no change; the parse hot path is not touched. Parse instruction count on use -g node@20 is 8,425 at base and 8,433 here (+0.1%), and wall time is flat at about 428 ns. Recursive help and KDL rendering measured +1–2.5% in one run, attributed to the character count no longer being inlined; those are cold paths.

Notes for review

The history is kept as small commits, one idea each, so each can be reviewed or reverted alone. Changes that need public field types to shrink (flag and command tables) would break the API; they are recorded for 8.0 in V8.md (#1550).

This description was generated by Claude Code.

🤖 Generated with Claude Code

AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5; version: unavailable.


Note

Low Risk
Refactor of cold-path string rendering with stated byte-for-byte output parity and conformance tests; no auth or data-handling changes.

Overview
Shrinks embedded usage-argv binary footprint for help, shell completion, and parse-error rendering by centralizing string work and avoiding per-call-site monomorphization—without changing rendered output.

Adds argv/src/text.rs with shared out-of-line helpers (push_all, char_count, same_name, split_once_eq, etc.) and routes help, completion, diagnostics, and spec code through them. Help drops write!/format! in favor of a PutStr trait, streaming wrap_lines, and flag spellings as Form (long/short) instead of allocating --name strings. Diagnostics use put_template / error_with hole templates and delegate stderr colour to help::Style::auto_stderr. Completion gains keep_prefixed, offer, explicit loops where iterator chains duplicated codegen, and crate::has_str / has_byte for alias checks.

Help template rendering applies palette mapping inside AnsiStyle::apply and writes semantics via semantic_into; invalid templates fall back to section substitution only.

Reviewed by Cursor Bugbot for commit 12e27eb. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Improvements
    • Completion matching now selects the longest matching sigil, with later declarations taking precedence when matches are equally long.
    • Help templates support palette-defined combined styles. Invalid style markup is displayed literally while placeholders are still substituted.
    • Diagnostic color selection now follows the same automatic color policy as help output.
    • Help and diagnostic output retain their existing wording and formats.

jdx and others added 30 commits October 14, 2026 16:51
Help rows, usage lines and annotations were assembled with write! and format!,
which pulls core::fmt argument handling into every call site. Plain pushes
through small out-of-line helpers produce the same bytes and a smaller binary.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
own_and_global built a String for every long and short spelling just to test
membership. A small Form enum compares them in place. Also drives the ANSI style
lookup from the published STYLES table.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
wrap returned a Vec<String> that every caller walked once. One wrapping routine
now hands each line to the writer, so the three callers share it and none of
them allocates the intermediate lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The sections-only fallback duplicated the template scanner and the blank-run
collapse. One loop now serves both.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The inline emphasis and template token tables were rebuilt on the stack for
every character; statics keep them out of the code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n notes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… new spec

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A page is written in hundreds of small pushes and every inlined push_str or push
carries its own capacity check and copy. Routing help text through one
out-of-line method each for strings and characters shrinks the binary; a help
page is far too cold for the call to show.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…site formatting

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… chain

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…r chains

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
jdx and others added 12 commits October 14, 2026 16:51
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… line

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sure

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…cept

The generated entry points collected `args_os` for the completion request and
again for the parse. The request is now asked of the vector the preamble
already holds, which drops a second allocation at startup and about 1 KB of
generated code per consumer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4XMMXCZ919RP4EF6CVJBS0X
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4XMQ010BT7VY1QSYVY6AX0G
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Central YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b13b1027-417c-4439-83f6-45c460c4af51
📥 Commits

Reviewing files that changed from the base of the PR and between a39e2b3 and 12e27eb.

📒 Files selected for processing: 8
  • argv/src/complete.rs
  • argv/src/diagnostic.rs
  • argv/src/help.rs
  • argv/src/help/template.rs
  • argv/src/lib.rs
  • argv/src/spec.rs
  • argv/src/text.rs
  • derive/src/codegen.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

📝 Walkthrough

The pull request refactors shared argument parsing helpers, completion matching and output, help rendering, and diagnostic formatting. Completion checks move into the shared parse preamble. Help wrapping streams output, and diagnostic messages use templates.

Changes: Argument parsing and terminal output

Layer / File(s) Summary
Shared parser lookup and text helpers
argv/src/lib.rs, argv/src/spec.rs, argv/src/text.rs
Adds shared text and membership helpers. Flag lookup and scope collection use a common ordered walk. Selector matching and sigil matching use explicit lookup paths.
Completion matching and interception
argv/src/complete.rs, derive/src/codegen.rs
Completion matching uses shared helpers. Completion requests use the arguments already collected by the shared parse preamble.
Completion candidates and shell output
argv/src/complete.rs
Candidate assembly and shell-word parsing use append helpers. Prefix filtering is centralized, and sigil selection retains the longest match, with later equal-length declarations taking precedence.
Help style and spelling rules
argv/src/help.rs, argv/src/help/template.rs, argv/src/spec.rs
Template styling resolves palette roles. Invalid style markup remains literal while placeholders are substituted. Flag forms represent long and short spellings.
Help page layout and wrapping
argv/src/help.rs
Help output uses shared append helpers and character counts. Wrapped text streams to callers instead of being collected into intermediate vectors.
Diagnostic message templates
argv/src/diagnostic.rs
Diagnostic formatting uses templates and the help module’s stderr color policy. Existing wording and color assignments are retained.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Refactor

Suggested reviewers: lu-zero

Merge Risk: ⚪ Minimal · up to 12e27

This is a size-focused refactor of help, completion and error rendering that is intended to leave output unchanged. No concrete merge-blocking risk was found in the supplied changes.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 66.46% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 164 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: reducing code size in help, completion, and error rendering within usage-argv.
  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issue was found.

Summary

This PR reduces repeated code in help, completion, errors, and flag lookup. Derived process entry points also collect arguments once.

  • Help pages use shared rendering code to build the same text.
  • Shell completion builds the same suggestions with smaller rendering paths.
  • Parse errors and warnings use shared templates to keep their text compact.

Reviews (1) · Last reviewed commit: "style(lib): drop needless borrows in hel..." · Reviewed by Greptile

@jdx
jdx merged commit 2cb3068 into main Oct 14, 2026
15 checks passed
@jdx
jdx deleted the perf/argv-size branch October 14, 2026 18:32
jdx added a commit that referenced this pull request Oct 14, 2026
…icing and searching (#1553)

<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/usage/trails/94
<!-- entire-trail-link-end -->

Second pass on the size of usage-argv's help rendering, which is still
the largest piece of what usage adds to a CLI binary. Stacked on #1551
(`perf/argv-size`); merge that first, then retarget.

### What changed

- The text wrapper was inlined into `page_sections` at every call.
Keeping `write_wrapped_indented` and `wrap_lines` out of line is the
single largest saving, about 21 KB.
- About 60 `&str[a..b]` sites in `help.rs` and `help/template.rs` each
carried a bounds check and a slice-error call. They go through three
small non-inlined helpers (`head`, `tail`, `slice`), about 5 KB.
- `own_and_global` builds flag spellings with plain helpers
(`push_forms`, `has_form`, `first_long`, ...) instead of iterator
chains, and the char and string searches use one byte scan rather than
core's searcher and its monomorphised copies.
- Line and `;`/`+` splitting use one `split_line`/`split_first`; style
names are one packed string; the prose span table is a `match` (no
relocations); and several small panic sites are gone.

Help output is byte-identical (plain, forced colour, and at width 200 on
oxfmt's real CLI) and the full workspace test suite passes.

### Measured

Linux x86_64, oxlint and oxfmt built from the oxc PR with usage-argv
patched in from this branch, exact loadable-section sums against the
parent branch: oxlint 14,906,504 → 14,863,744 B (-42,760), oxfmt
9,243,420 → 9,200,652 B (-42,768).

Speed: interleaved min-of-500 `--help` is 769 µs against 780 µs for the
parent (parity; two copies of the same binary differ by about 35 µs
here). Instruction count for `oxlint --help` under cachegrind is +0.7%
(1,637,947 → 1,648,957), the price of the byte scan's first-byte filter.
Parse paths are untouched.

Ideas that need public API changes (for 8.0) are in #1550.

_This description was generated by Claude Code._

🤖 Generated with [Claude Code](https://claude.com/claude-code)

*AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5;
version: unavailable.*

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Cold-path help rendering only, with stated byte-identical output;
parse and argv handling are unchanged aside from `push_forms` in spec
layout.
> 
> **Overview**
> **Shrinks the usage-argv help renderer binary footprint** by routing
repeated string work through shared, `#[inline(never)]` helpers
(`head`/`tail`/`slice`, `find_str`/`find_byte`, `lines_of`,
`split_line`, a `Text` trait for trim/blank checks) instead of
duplicating `core` slicing and search at dozens of call sites in
`help.rs` and `help/template.rs`.
> 
> **Template and inline styling** drop static string tables for span
kinds and style names (packed `span_kind` match + `STYLE_NAMES` with
`style_index`), and flag spelling collection uses `push_forms` / small
lookup helpers rather than iterator chains.
> 
> **`page()`** now resolves path/chain once then calls `assembled_help`
/ `recursive_help` for both routed and address-based lookup; a few panic
paths become graceful `Option` handling (empty chain, implicit
positional clause).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ace6eb0. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Help output now handles empty command chains and missing or
unavailable items without failing.
* Implicit clauses omit the argument synopsis when no visible argument
is available.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ███▁▁▁▁▁▁▁▁▁▁▁▁▁ 266,454,587 → 266,450,432 -0.00% 47.24 → 47.38ms +0.30%
startup ▇██████▇▇▇████▇▁ 1,055,683 → 1,024,352 -2.97% 1.50 → 1.52ms +1.24%

No instruction-count regression above 1%.

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

Shadow comparison

Parsing mise use -g node@20 against a shadow of mise's committed spec.
Reported, not gated: the shadow grows as the derive learns to express more, so
what to watch is the ratio rather than either column.

framework stripped binary, bytes
usage 1250880
bpaf 2494640
clap 3100904
framework instructions, cold parse vs usage
usage 8293 —
clap 6313736 761x
bpaf 21909300 2641x
                                              min       p01       p10    median
usage-rs: argv -> struct                      762       822       826       836  ns
clap: build tree + parse -> struct        1244801   1252035   1262114   1279581  ns
bpaf: build parser + parse -> struct      3475816   3475816   3495815   3517909  ns

usage: argv -> struct                             829 ns      0.83 µs
clap: build tree + parse -> struct            1266503 ns   1266.50 µs
clap: parse -> struct, tree reused              50455 ns     50.45 µs
clap: build tree only                          750985 ns    750.99 µs

12e27eb1f27a vs a39e2b33b3ae · measured on the runner, not pushed to the history.

jdx pushed a commit that referenced this pull request Oct 14, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/usage/trails/93
<!-- entire-trail-link-end -->

### 📚 Documentation

- replace V7.md with V8.md for pending breaking changes by
[@jdx](https://github.com/jdx) in
[#1550](#1550)

### ⚡ Performance

- **(complete)** shrink completion splitting and error suggestions by
[@jdx](https://github.com/jdx) in
[#1554](#1554)
- **(lib)** shrink the help, completion and error rendering code in
usage-argv by [@jdx](https://github.com/jdx) in
[#1551](#1551)
- **(lib)** shrink the help renderer further by outlining wrapping,
slicing and searching by [@jdx](https://github.com/jdx) in
[#1553](#1553)

### 📦️ Dependency Updates

- bump jactionlint to 2.2.0 and mbx to 1.24.0 by
[@jdx](https://github.com/jdx) in
[#1549](#1549)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant