Repository navigation
perf: shrink the help and completion code usage-rs adds to a binary - #1469
Conversation
Share one index-based merge sort across every cold-path list instead of instantiating std's sort per element type, run the help page's section grouping through one non-generic body instead of inlining it at six call sites, and let view lookup fold away for CLIs that declare no views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe pull request adds a shared stable sorting utility and applies it to completion, diagnostic, and help code. It also refactors help section grouping and changes spec view lookup when no views are declared. ChangesSorting and Help Generation
Spec View Lookup
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Refactor Suggested reviewers: Merge Risk: ⚪ Minimal · up to The sorting, help grouping, and empty-view fast path preserve their described behavior. The change is ready to merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
|
Instruction counts
1 benchmark(s) above the 1% gate: 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 comparisonParsing
|
…1470) <!-- entire-trail-link-start --> https://entire.io/gh/jdx/usage/trails/22 <!-- entire-trail-link-end --> Every flag in a usage-rs CLI gets a static `FlagMeta` record in the binary. Before this change it was 664 bytes: 58 fields, nearly all empty for a typical flag, which usually declares only help, a heading and maybe a default or choices. With oxlint's 47 flags plus the built-in help/version entries, that was ~35 KB of mostly zeros. After [#1469](#1469), it was the largest remaining part of what usage-rs adds to oxlint and oxfmt ([oxc-project/oxc#26027](oxc-project/oxc#26027)). ### What changed The 35 rarely declared fields move into a new `FlagExtra` struct behind `FlagMeta::extra`. They cover: - relations: `conflicts`, `requires*`, `required_if*`, `required_unless*`, `overrides`, `exclusive`, `default_if`; - deprecation, including `deprecated_env`; - validation; - value bounds and `delimiter`; - hidden aliases, `display_order`, `admonitions`, `value_names`, `env_fallback`, `hide_env_values`; - spec-only metadata: `choice_aliases`, `choice_details`, `complete_type`, `surface`, `available_if`, `effect`. `FlagMeta` itself is now 168 bytes. A flag that declares none of these points at one shared static, `NO_FLAG_EXTRA`. The derive can't always tell at expansion time whether a flag's extras are empty, because choice aliases can come from a `ValueEnum`'s trait constants. So it builds each flag's extras as a `const`, and the `const fn` `FlagExtra::shared` swaps an empty one for the shared static at compile time. `shared` destructures every field without `..`, so adding a field to `FlagExtra` without updating the check is a compile error, not silently dropped metadata. ### For code that uses these types directly Derived CLIs need no changes. Code that reads a moved field goes through `extra`: ```rust // before if let Some(reason) = meta.deprecated { /* … */ } // after if let Some(reason) = meta.extra.deprecated { /* … */ } ``` Hand-written tables set the moved fields in a nested literal. In a `static` the reference is promoted, as with the table's other slices: ```rust FlagMeta { flag: &OLD, help: Some("Use the old mode"), extra: &FlagExtra { deprecated: Some("use --new"), ..FlagExtra::EMPTY }, ..FlagMeta::EMPTY } ``` `FlagMeta::EMPTY` already points at the shared instance, so `..FlagMeta::EMPTY` keeps working for flags with no extras. ### Measured effect Measured on oxc's PR branch, with its usage crates patched to this branch, using oxc's release profile (`opt-level = 3`, fat LTO) on linux x86_64. Numbers are summed allocated sections of the stripped binaries, compared with #1469 alone: | | oxlint | oxfmt | | --- | ---: | ---: | | `.data.rel.ro` | −42,624 B | −14,880 B | | `.rela.dyn` (one pointer per flag) | +1,992 B | +624 B | | `.text` + `.rodata` | +736 B | +624 B | | **Total** | **−39,896 B** | **−13,632 B** | Together with #1469, usage-rs's overhead over bpaf drops from +219,300 B to +138,372 B on oxlint (−37%) and from +169,148 B to +114,284 B on oxfmt (−32%). Output is unchanged. On both oxc binaries, I compared help (plain and coloured), `--version`, error messages, `__usage_spec__` and completions before and after, and all 24 cases were byte-identical. The workspace suite passes (2,800 tests), with no snapshot changes, and clippy is clean. A new test pins that empty extras resolve to the shared instance and that anything declared, even a lone `bool`, keeps its own copy. `ArgMeta` and `CommandMeta` keep their layout. oxlint has one positional and ten command records, so the same split would save a few KB at most. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Wide refactor of flag metadata layout and every consumer path (help, completion, spec emission, derive); behavior should be identical but any missed `extra.` migration would be a subtle runtime bug. > > **Overview** > **Shrinks per-flag static metadata** by moving ~35 rarely set fields (relations, deprecation, validation, hidden aliases, spec-only choice/completion data, effects, etc.) from inline `FlagMeta` into a new `FlagExtra`, referenced by `FlagMeta::extra`. Common flags with no extras point at a single shared `NO_FLAG_EXTRA`; derive and table builders construct a per-flag `FlagExtra` and run `FlagExtra::shared` so empty extras collapse to that static at compile time. > > **Call sites** in completion, help rendering, KDL/spec emission, conformance table building, and tests now read those fields through `meta.extra.*` instead of on `FlagMeta` directly. `help_heading` stays on `FlagMeta`. Hand-written `FlagMeta` literals nest an `extra: &FlagExtra { … }` block; generated CLIs are unchanged at the source level. > > No intended behavior change—same help, completions, and spec output—with the goal of much smaller `.data.rel.ro` in large CLIs. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e3c76b6. 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 * **Refactor** * Command-line help, completion, and flag behavior remain unchanged. Existing flag details, such as help text, choices, visibility, and deprecation notices, continue to appear as before. * No new end-user features or behavior changes are included in this update. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
<!-- entire-trail-link-start --> https://entire.io/gh/jdx/usage/trails/16 <!-- entire-trail-link-end --> ### 🐛 Bug Fixes - **(help)** colour only the structure the renderer writes by [@jdx](https://github.com/jdx) in [#1480](#1480) ### ⚡ Performance - shrink the help and completion code usage-rs adds to a binary by [@jdx](https://github.com/jdx) in [#1469](#1469) - keep rarely declared flag metadata out of every flag's table by [@jdx](https://github.com/jdx) in [#1470](#1470) - write help rows through one renderer for both help pages by [@jdx](https://github.com/jdx) in [#1471](#1471) - collect coloured-help structure while the page is written by [@jdx](https://github.com/jdx) in [#1476](#1476) - shrink the per-CLI code the derive generates for binding by [@jdx](https://github.com/jdx) in [#1477](#1477) - shrink the parse-error renderer usage-rs adds to a binary by [@jdx](https://github.com/jdx) in [#1478](#1478) - shrink the shell-completion code a derived CLI carries by [@jdx](https://github.com/jdx) in [#1475](#1475) - keep rarely declared command metadata out of every command's table by [@jdx](https://github.com/jdx) in [#1472](#1472) - shrink the command and metadata tables a derive emits by [@jdx](https://github.com/jdx) in [#1474](#1474) ### 🔍 Other Changes - **(ci)** comment on a discussion when the PR implementing it merges by [@jdx](https://github.com/jdx) in [#1460](#1460) ### 📦️ Dependency Updates - update jdx/packslip action to v1.2.0 by [@renovate[bot]](https://github.com/renovate[bot]) in [#1466](#1466) - update zizmorcore/zizmor-action action to v0.6.4 by [@renovate[bot]](https://github.com/renovate[bot]) in [#1465](#1465) - update module github.com/urfave/cli/v3 to v3.12.0 by [@renovate[bot]](https://github.com/renovate[bot]) in [#1467](#1467) - update aube to v2.3.0 by [@jdx](https://github.com/jdx) in [#1473](#1473)
https://entire.io/gh/jdx/usage/trails/21
A CLI built with usage-rs carries its help, completion and diagnostics renderers in its release binary. In oxlint and oxfmt (oxc-project/oxc#26027), that overhead is what reviewers weigh against bpaf. This PR makes those renderers smaller without changing anything they print.
What changed
slice::sort_*. That instantiated a full quicksort for six different element types, about 35 KB of code. They now share one bottom-up merge sort over indices behind adyncomparator (argv/src/order.rs). Each element type only adds a small loop of swaps. The sort is stable where the old one was not, which changes nothing a user sees: every comparator either orders completely or is followed by a dedup of equal items.split_groups_sectiontook iterators and closures generically, so the grouping loop was inlined into all six help-page call sites. It is now a thin generic shell over one#[inline(never)]body.view_for_programis#[inline]and returns early when a spec declares no views. For a CLI without views, the derive's view-rewriting branches become dead code and are dropped.Measured effect
Measured on oxc's PR branch, with its usage crates patched to this branch, using oxc's release profile (
opt-level = 3, fat LTO, one codegen unit) on linux x86_64. Numbers are summed allocated sections of the stripped binaries (size), because stripped file sizes only move in 4 KB steps.mainOutput is unchanged. On both binaries I compared, byte for byte,
--help,-h, coloured--help,--version, unknown-flag and invalid-value errors,__usage_spec__and__complete_word__for bash, zsh and fish, before and after this change. The workspace suite passes (cargo test --all --all-features, 2,799 tests), with no snapshot changes, and clippy is clean. The perf gate's benchmarks run theusageCLI, which does not go through usage-argv, so they are not expected to move.What remains
Most of oxlint's remaining overhead is static data rather than code: each
FlagMetais 664 bytes, mostly empty fields. Moving the rarely set fields behind a shared pointer would change the publicFlagMeta/CommandMetafields, so that is left for a separate PR.🤖 Generated with Claude Code
Note
Low Risk
Binary-size and internal sorting/refactor only; PR asserts byte-identical help, completion, and error output with full test suite passing.
Overview
Shrinks usage-argv help, completion, and diagnostic code in release binaries by deduplicating sorting and help-section layout, without changing rendered output.
Shared stable sort (
argv/src/order.rs). Cold-path lists (completer names, completion candidates, “did you mean” scores, help usage rows) no longer callslice::sort_unstable*per element type. They route through one index-based merge sort behind adyncomparator so LLVM does not monomorphize multiple quicksorts (~35 KB cited in the PR). Sorts become stable; callers that dedup equal keys are unchanged in user-visible behavior.Help section grouping.
split_groups_sectionis a thin generic wrapper over a single#[inline(never)]grouped_sectionsthat indexes slices instead of cloning iterators—one copy of the grouping loop instead of six inlined variants. Localsort_rowsinhelp.rsis removed in favor ofcrate::order::sort_by.View dispatch.
view_for_programis#[inline]and returnsNoneimmediately whenspec.viewsis empty so CLIs with no executable views can drop dead view-rewriting branches at compile time.Reviewed by Cursor Bugbot for commit 8a50714. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit