Context
After implementing seven startup optimizations (#573, #574, #575, #576, #577, #579), re-profiling with async-profiler shows that ProcessedOption.createDirect() is now the #1 aesh method in both CPU and allocation profiles:
- Container CPU: 18.5% (248 samples out of 1340)
- Container allocations: 69.8% (4124 samples out of 5908) —
GeneratedProcessedOption objects
- Full startup CPU: 2.1% (96 samples out of 4556)
- Full startup allocations: 47.0% (902 samples out of 1921)
This is expected — after eliminating avoidable overhead (lazy map builds, NO_COLOR lookups, clearOptions on clean commands, per-value converter allocation), the irreducible cost of constructing ProcessedOption objects dominates.
What createDirect does
public static ProcessedOption createDirect(...) {
ProcessedOption opt = new GeneratedProcessedOption(); // 1 allocation (~280 bytes, ~35 fields)
opt.shortName = shortName; // 7 direct field assignments
opt.name = name;
opt.description = description;
opt.type = type;
opt.fieldName = fieldName;
opt.optionType = optionType;
opt.converter = converter;
opt.fieldAccessor = fieldAccessor;
return opt;
}
The no-arg constructor adds 5 more field assignments (selectorType, valueSeparator, properties, values, defaultValues). Then the generated code calls setter methods for non-default attributes (setRequired, setNegatable, setInherited, etc.).
For the jbang-like CLI with 12 commands and ~100+ total options, this means ~100 GeneratedProcessedOption allocations per startup.
Potential Optimization Approaches
1. Split ProcessedOption into template + mutable state (Medium effort, medium impact)
Separate the immutable metadata (name, description, type, converter, fieldAccessor, required, negatable, negationPrefix, inherited, etc.) from the mutable parse state (values, properties, cursorOption, negatedByUser, longNameUsed, endsWithSeparator).
The template could be created once and shared across invocations for the same command class. Only the mutable state wrapper (~10 fields instead of ~35) would be allocated per invocation. This would roughly halve the per-option allocation.
Tradeoff: Significant refactoring — every reader of ProcessedOption fields would need to go through the template for metadata vs the state for parse values. The clear() method would only reset the state wrapper.
2. Reduce field count via bit packing (Low effort, low impact)
Several fields have small value domains and are rarely overridden from defaults:
| Field |
Type |
Default |
Could be |
selectorType |
enum (5 values) |
NO_OP |
3 bits in flags |
visibility |
enum (3 values) |
BRIEF |
2 bits in flags |
completeFallback |
enum (4 values) |
DEFAULT |
2 bits in flags |
required |
boolean |
false |
1 bit |
negatable |
boolean |
false |
1 bit |
inherited |
boolean |
false |
1 bit |
optionalValue |
boolean |
false |
1 bit |
optionalWrapped |
boolean |
false |
1 bit |
askIfNotSet |
boolean |
false |
1 bit |
acceptNameWithoutDashes |
boolean |
false |
1 bit |
overrideRequired |
boolean |
false |
1 bit |
isUrl |
boolean |
false |
1 bit |
ansiMode |
boolean |
true |
1 bit |
All 13 fields fit in a single int (16 bits used). This eliminates 12 fields × 1 byte each = 12 bytes, but with object alignment it may save 0-16 bytes depending on JVM layout. CPU impact is minimal — the getter would do a bit mask instead of a field read.
3. Null-default pattern for rarely-used String/Object fields (Low effort, low impact)
Fields like negationPrefix (default "no-"), helpGroup (default ""), descriptionUrl (default null) could use null as the default and have getters return the default value:
// Instead of: private String negationPrefix = "no-";
private String negationPrefix; // null means "no-"
public String getNegationPrefix() { return negationPrefix != null ? negationPrefix : "no-"; }
Saves the reference assignment per option for rarely-overridden fields.
4. Pre-build at compile time (High effort, high impact)
The annotation processor could generate the entire ProcessedCommand with pre-built ProcessedOption arrays as static constants. Each invocation would deep-clone the template rather than rebuilding from scratch. This is the most impactful but also the most architecturally invasive approach — deferred for Quarkus compatibility concerns.
Current Performance Baseline
After all optimizations in this session:
| Metric |
Value |
| Full startup per command (jbang-like, 12 subcommands) |
8.7 us/cmd |
| Previous baseline (before optimizations) |
24.0 us/cmd |
| Improvement |
2.8x faster |
| Application CPU (excluding JIT+GC) |
~20% of profile |
| JIT compilation |
~45% of profile |
| GC/native overhead |
~35% of profile |
The remaining aesh application work is well-distributed across createDirect (option construction), buildLookupMaps (map construction), populateObject (value injection), and resetField (field reset). No single method dominates enough for a targeted micro-optimization to yield significant gains.
Recommendation
Approach #1 (template + state split) offers the best balance of impact and feasibility. It could reduce per-option allocation by ~50% and improve cache locality since parse-hot fields would be co-located. However, it is a significant refactoring effort and should be done as a dedicated feature branch with thorough testing.
Context
After implementing seven startup optimizations (#573, #574, #575, #576, #577, #579), re-profiling with async-profiler shows that
ProcessedOption.createDirect()is now the #1 aesh method in both CPU and allocation profiles:GeneratedProcessedOptionobjectsThis is expected — after eliminating avoidable overhead (lazy map builds, NO_COLOR lookups, clearOptions on clean commands, per-value converter allocation), the irreducible cost of constructing
ProcessedOptionobjects dominates.What createDirect does
The no-arg constructor adds 5 more field assignments (selectorType, valueSeparator, properties, values, defaultValues). Then the generated code calls setter methods for non-default attributes (setRequired, setNegatable, setInherited, etc.).
For the jbang-like CLI with 12 commands and ~100+ total options, this means ~100
GeneratedProcessedOptionallocations per startup.Potential Optimization Approaches
1. Split ProcessedOption into template + mutable state (Medium effort, medium impact)
Separate the immutable metadata (name, description, type, converter, fieldAccessor, required, negatable, negationPrefix, inherited, etc.) from the mutable parse state (values, properties, cursorOption, negatedByUser, longNameUsed, endsWithSeparator).
The template could be created once and shared across invocations for the same command class. Only the mutable state wrapper (~10 fields instead of ~35) would be allocated per invocation. This would roughly halve the per-option allocation.
Tradeoff: Significant refactoring — every reader of
ProcessedOptionfields would need to go through the template for metadata vs the state for parse values. Theclear()method would only reset the state wrapper.2. Reduce field count via bit packing (Low effort, low impact)
Several fields have small value domains and are rarely overridden from defaults:
selectorTypeNO_OPvisibilityBRIEFcompleteFallbackDEFAULTrequirednegatableinheritedoptionalValueoptionalWrappedaskIfNotSetacceptNameWithoutDashesoverrideRequiredisUrlansiModeAll 13 fields fit in a single
int(16 bits used). This eliminates 12 fields × 1 byte each = 12 bytes, but with object alignment it may save 0-16 bytes depending on JVM layout. CPU impact is minimal — the getter would do a bit mask instead of a field read.3. Null-default pattern for rarely-used String/Object fields (Low effort, low impact)
Fields like
negationPrefix(default"no-"),helpGroup(default""),descriptionUrl(default null) could use null as the default and have getters return the default value:Saves the reference assignment per option for rarely-overridden fields.
4. Pre-build at compile time (High effort, high impact)
The annotation processor could generate the entire
ProcessedCommandwith pre-builtProcessedOptionarrays as static constants. Each invocation would deep-clone the template rather than rebuilding from scratch. This is the most impactful but also the most architecturally invasive approach — deferred for Quarkus compatibility concerns.Current Performance Baseline
After all optimizations in this session:
The remaining aesh application work is well-distributed across
createDirect(option construction),buildLookupMaps(map construction),populateObject(value injection), andresetField(field reset). No single method dominates enough for a targeted micro-optimization to yield significant gains.Recommendation
Approach #1 (template + state split) offers the best balance of impact and feasibility. It could reduce per-option allocation by ~50% and improve cache locality since parse-hot fields would be co-located. However, it is a significant refactoring effort and should be done as a dedicated feature branch with thorough testing.