Skip to content

Cache option lookup maps in ProcessedCommand instead of rebuilding per parse #573

Description

@stalep

Updated Understanding (2026-08-17)

Profiling investigation revealed that the lookup maps are already lazily cached and NOT rebuilt per parse cycle. The clear() method (called between parses) intentionally does not invalidate the lookup maps. The buildLookupMaps samples in the async-profiler output come from:

  1. Newly created child parsers -- resolveAllLazyChildren() creates brand-new ProcessedCommand objects for each subcommand (with null maps), and their first option lookup triggers buildLookupMaps(). This is actually Cache resolved lazy children instead of rebuilding child parsers on every parse #577 (cache resolved lazy children).
  2. First-time map build on fresh containers -- In single-shot CLI mode (AeshRuntimeRunner), each JVM invocation creates a fresh container, so the maps are built once on the first findLongOption/findBareLongOption call during parsing.

The actual optimization here is smaller than originally estimated: build the lookup maps eagerly at construction time instead of lazily on first lookup, so the cost is paid during container creation rather than during the first parse. This also eliminates the wasted build-invalidate cycles in doGenerateHelp()/doGenerateVersion() (which call findOption() triggering a map build, then immediately invalidate the maps).

Original Problem (corrected)

ProcessedCommand.buildLookupMaps() creates four HashMap instances and populates them from the options list. Currently this happens lazily on the first findOption/findLongOption/findBareLongOption call. The constructor has a wasteful pattern:

constructor → addOption() × N → maps stay null
           → doGenerateHelp() → findOption("help") triggers buildLookupMaps()
                              → adds help option → invalidateLookupMaps()
           → doGenerateVersion() → findOption("version") triggers buildLookupMaps()
                                 → adds version option → invalidateLookupMaps()
           → (end) maps are null again → rebuilt on first parse lookup

Proposed Fix

  1. Add buildLookupMaps() as the last line of the constructor, after all options (including help/version) are added
  2. Change doGenerateHelp()/doGenerateVersion() to use a direct linear scan for the duplicate check instead of findOption(), avoiding the premature map build
  3. Remove the invalidateLookupMaps() calls from those methods (maps have not been built yet)

Impact

Small but clean improvement. Eliminates the lazy-init null-check overhead on every find*Option() call during parsing, and removes the wasted build-invalidate-rebuild cycle during construction.

Note: The bigger win is #577 (cache resolved lazy children), which eliminates the creation of 11 new child ProcessedCommand objects per parse iteration entirely. Implement #577 first, then re-profile to assess if #573 still provides measurable benefit.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions