blob: e76b2eba7410695bbd7420ad0bf22f36b804b238 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package jakarta.faces.component;
import org.apache.myfaces.core.api.shared.EditableValueHolderState;
import java.util.Arrays;
/**
* Microbenchmark for the UIRepeat per-row state-save/restore cycle and the full
* JSF lifecycle phases (APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES).
*
* <p>Run as a plain {@code main()} — no JMH, no test framework needed.
* To compare before/after the TransientStateHelper change:
* <ol>
* <li>Run with current code (after the change).</li>
* <li>{@code git stash} to revert to StateHelper-based code.</li>
* <li>Run again and compare.</li>
* </ol>
*
* <p>No FacesContext is required: all benchmarked paths operate purely on
* in-memory fields and do not call {@code _component.getFacesContext()}.
*
* <p>Scenarios exercised:
* <ul>
* <li><b>getter/setter hot path</b> – {@code setValid/isValid},
* {@code setSubmittedValue/getSubmittedValue},
* {@code setLocalValueSet/isLocalValueSet}.</li>
* <li><b>{@code saveTransientState}/{@code restoreTransientState}</b> –
* UIRepeat's {@code _rowTransientStates} path.</li>
* <li><b>{@code EditableValueHolderState} roundtrip</b> –
* UIRepeat's {@code _rowStates} (EVH) path.</li>
* <li><b>Full row-switch (render)</b> – EVH save+restore per row, read-only.</li>
* <li><b>APPLY_REQUEST_VALUES</b> – restore → decode (setSubmittedValue) → save.</li>
* <li><b>PROCESS_VALIDATIONS success</b> – restore → validate OK (setValue + clear sv) → save.</li>
* <li><b>PROCESS_VALIDATIONS failure</b> – restore → validate fail (setValid(false)) → save.</li>
* <li><b>UPDATE_MODEL_VALUES</b> – restore → updateModel (setValue(null)) → save.</li>
* </ul>
*/
public class UIRepeatBenchmark
{
static final int ROWS = 10;
static final int WARMUP = 300_000;
static final int MEASURE = 3_000_000;
static final int ROUNDS = 12;
/** Pre-allocated submitted-value strings — avoids String allocation inside benchmarked loops. */
static final String[] SUBMITTED_VALUES = new String[ROWS];
static
{
for (int i = 0; i < ROWS; i++)
{
SUBMITTED_VALUES[i] = "field-value-" + i;
}
}
// ── runner ────────────────────────────────────────────────────────────────
static double run(String label, Runnable bench)
{
for (int i = 0; i < WARMUP; i++)
{
bench.run();
}
long[] times = new long[ROUNDS];
for (int r = 0; r < ROUNDS; r++)
{
long t0 = System.nanoTime();
for (int i = 0; i < MEASURE; i++)
{
bench.run();
}
times[r] = System.nanoTime() - t0;
}
Arrays.sort(times);
double ns = (double) times[ROUNDS / 2] / MEASURE;
System.out.printf(" %-50s %6.2f ns/op%n", label, ns);
return ns;
}
static void header(String title)
{
System.out.println("\n=== " + title + " ===");
}
// ── helpers ───────────────────────────────────────────────────────────────
/** Reset component back to all-default state between sub-benchmarks. */
static void resetInput(UIInput input)
{
input.setValid(true);
input.setLocalValueSet(false);
input.setSubmittedValue(null);
}
/**
* Pre-populate N per-row snapshots that simulate a realistic mix:
* some rows are clean (all defaults), others have validation errors.
*/
static Object[] buildTransientSnapshots(UIInput input, int n)
{
Object[] snaps = new Object[n];
for (int i = 0; i < n; i++)
{
if (i % 3 == 0)
{
// row has a validation error
input.setValid(false);
input.setSubmittedValue("bad-value-" + i);
input.setLocalValueSet(false);
}
else
{
// row is clean
input.setValid(true);
input.setSubmittedValue(null);
input.setLocalValueSet(false);
}
snaps[i] = input.saveTransientState(null);
}
resetInput(input);
return snaps;
}
static EditableValueHolderState[] buildEvhSnapshots(UIInput input, int n)
{
EditableValueHolderState[] snaps = new EditableValueHolderState[n];
for (int i = 0; i < n; i++)
{
if (i % 3 == 0)
{
input.setValid(false);
input.setSubmittedValue("bad-value-" + i);
}
else
{
input.setValid(true);
input.setSubmittedValue(null);
}
snaps[i] = EditableValueHolderState.create(input);
}
resetInput(input);
return snaps;
}
// ── main ──────────────────────────────────────────────────────────────────
public static void main(String[] args)
{
UIInput input = new UIInput();
// ── 1. getter / setter hot path ───────────────────────────────────────
header("getter/setter hot path (non-default state, matches validate() path)");
run("setValid(false) + isValid()", () ->
{
input.setValid(false);
boolean v = input.isValid();
});
run("setValid(true) + isValid() [default, no-op path]", () ->
{
input.setValid(true);
boolean v = input.isValid();
});
run("setSubmittedValue(str) + getSubmittedValue()", () ->
{
input.setSubmittedValue("hello");
Object sv = input.getSubmittedValue();
});
run("setSubmittedValue(null) + getSubmittedValue() [clear path]", () ->
{
input.setSubmittedValue(null);
Object sv = input.getSubmittedValue();
});
run("setLocalValueSet(true) + isLocalValueSet()", () ->
{
input.setLocalValueSet(true);
boolean lv = input.isLocalValueSet();
});
run("all three setters + getters combined", () ->
{
input.setValid(false);
input.setSubmittedValue("v");
input.setLocalValueSet(false);
boolean v = input.isValid();
Object sv = input.getSubmittedValue();
boolean lv = input.isLocalValueSet();
});
// ── 2. saveTransientState + restoreTransientState ─────────────────────
// This is UIRepeat's _rowTransientStates path: one save+restore per
// child component per row switch.
header("saveTransientState + restoreTransientState (UIRepeat _rowTransientStates path)");
// clean state (null snapshot — the common case for valid, unsubmitted rows)
resetInput(input);
run("save+restore [all defaults — null snapshot]", () ->
{
Object s = input.saveTransientState(null);
input.restoreTransientState(null, s);
});
// dirty state (validation error — non-null snapshot)
input.setValid(false);
input.setSubmittedValue("bad");
run("save+restore [valid=false, submittedValue set]", () ->
{
Object s = input.saveTransientState(null);
input.restoreTransientState(null, s);
});
resetInput(input);
// ── 3. EditableValueHolderState roundtrip ─────────────────────────────
// This is UIRepeat's _rowStates (EVH) path: UIRepeat calls
// EditableValueHolderState.create(evh) when leaving a row and
// state.restoreState(evh) when entering the next row.
header("EditableValueHolderState roundtrip (UIRepeat _rowStates path)");
// clean row — create() returns null, EMPTY.restoreState() is used
resetInput(input);
run("create [defaults → null] + EMPTY.restoreState", () ->
{
EditableValueHolderState s = EditableValueHolderState.create(input);
// s is null for default state; UIRepeat uses EMPTY.restoreState
EditableValueHolderState.EMPTY.restoreState(input);
});
// dirty row — create() allocates a new object
input.setValid(false);
input.setSubmittedValue("bad-value");
run("create [non-default] + restoreState", () ->
{
EditableValueHolderState s = EditableValueHolderState.create(input);
s.restoreState(input);
});
resetInput(input);
// ── 4a. Full iteration — MODERN path ─────────────────────────────────
// _setIndexWithoutPreserveComponentState: only EditableValueHolderState,
// NO saveTransientState/restoreTransientState on children.
// This is the path taken by virtually all real UIRepeat usage.
header("Full row-switch — MODERN path (" + ROWS + " rows, EVH only)");
EditableValueHolderState[] evhSnaps = buildEvhSnapshots(input, ROWS);
run("full iteration (EVH save+restore per row only)", () ->
{
for (int row = 0; row < ROWS; row++)
{
// --- UIRepeat: entering row (restoreChildStates) ---
EditableValueHolderState evh = evhSnaps[row];
if (evh != null)
{
evh.restoreState(input);
}
else
{
EditableValueHolderState.EMPTY.restoreState(input);
}
// --- (validation / rendering would happen here) ---
boolean valid = input.isValid();
Object sv = input.getSubmittedValue();
// --- UIRepeat: leaving row (saveChildStates) ---
evhSnaps[row] = EditableValueHolderState.create(input);
}
});
// ── 4b. Full iteration — LEGACY path ──────────────────────────────────
// _setIndexWithPreserveComponentState: only _rowTransientStates via
// saveTransientState/restoreTransientState on children.
// Only active when rowStatePreserved=true.
header("Full row-switch — LEGACY path (" + ROWS + " rows, transient only)");
Object[] transientSnaps = buildTransientSnapshots(input, ROWS);
run("full iteration (transient save+restore per row only)", () ->
{
for (int row = 0; row < ROWS; row++)
{
// --- UIRepeat: entering row (restoreTransientDescendantComponentStates) ---
input.restoreTransientState(null, transientSnaps[row]);
// --- (validation / rendering would happen here) ---
boolean valid = input.isValid();
Object sv = input.getSubmittedValue();
// --- UIRepeat: leaving row (saveTransientDescendantComponentStates) ---
transientSnaps[row] = input.saveTransientState(null);
}
});
// ── 5a. APPLY_REQUEST_VALUES ───────────────────────────────────────────
// Simulates UIRepeat.processDecodes(): for each row, restore EVH state,
// let the renderer decode the request (setSubmittedValue), then save EVH state.
// This is where Mojarra's plain-field submittedValue has a structural advantage:
// its setSubmittedValue is a direct field write; ours goes through TransientStateHelper.
header("Full row-switch — APPLY_REQUEST_VALUES (" + ROWS + " rows)");
// Start with empty snapshots (fresh request, no prior per-row state).
EditableValueHolderState[] applySnaps = new EditableValueHolderState[ROWS];
resetInput(input);
run("restore → decode (setSubmittedValue) → save", () ->
{
for (int row = 0; row < ROWS; row++)
{
// UIRepeat: entering row — restore previous EVH state (null on first visit)
EditableValueHolderState prevEvh = applySnaps[row];
if (prevEvh != null)
{
prevEvh.restoreState(input);
}
else
{
EditableValueHolderState.EMPTY.restoreState(input);
}
// Simulate renderer decode: sets the submitted form value on the component
input.setSubmittedValue(SUBMITTED_VALUES[row]);
// UIRepeat: leaving row — save updated EVH state
applySnaps[row] = EditableValueHolderState.create(input);
}
});
// ── 5b. PROCESS_VALIDATIONS — success ─────────────────────────────────
// Simulates UIInput.validate() succeeding: reads submittedValue, converts it,
// calls setValue(convertedValue) [which also sets localValueSet=true],
// then clears submittedValue.
header("Full row-switch — PROCESS_VALIDATIONS success (" + ROWS + " rows)");
// Seed snapshots as if APPLY_REQUEST_VALUES already ran: each row has a submittedValue.
EditableValueHolderState[] validateOkSnaps = new EditableValueHolderState[ROWS];
for (int i = 0; i < ROWS; i++)
{
input.setSubmittedValue(SUBMITTED_VALUES[i]);
validateOkSnaps[i] = EditableValueHolderState.create(input);
}
resetInput(input);
run("restore → validate OK (setValue + clear sv) → save", () ->
{
for (int row = 0; row < ROWS; row++)
{
EditableValueHolderState evh = validateOkSnaps[row];
if (evh != null)
{
evh.restoreState(input);
}
else
{
EditableValueHolderState.EMPTY.restoreState(input);
}
// Simulate UIInput.validate() success path:
// converted = getConvertedValue(...) → skipped (no FacesContext needed)
Object sv = input.getSubmittedValue(); // read submitted value
input.setValue(sv); // store as local value (also sets localValueSet=true)
input.setSubmittedValue(null); // clear submitted value after successful conversion
validateOkSnaps[row] = EditableValueHolderState.create(input);
}
});
// ── 5c. PROCESS_VALIDATIONS — failure ─────────────────────────────────
// Simulates UIInput.validate() failing: reads submittedValue, marks invalid,
// keeps submittedValue for re-display in the error state.
header("Full row-switch — PROCESS_VALIDATIONS failure (" + ROWS + " rows)");
// Seed snapshots with submitted values.
EditableValueHolderState[] validateFailSnaps = new EditableValueHolderState[ROWS];
for (int i = 0; i < ROWS; i++)
{
input.setSubmittedValue(SUBMITTED_VALUES[i]);
validateFailSnaps[i] = EditableValueHolderState.create(input);
}
resetInput(input);
run("restore → validate fail (setValid(false)) → save", () ->
{
for (int row = 0; row < ROWS; row++)
{
EditableValueHolderState evh = validateFailSnaps[row];
if (evh != null)
{
evh.restoreState(input);
}
else
{
EditableValueHolderState.EMPTY.restoreState(input);
}
// Simulate UIInput.validate() failure path:
// submittedValue is retained; component is marked invalid
Object sv = input.getSubmittedValue(); // read (retained for error display)
input.setValid(false); // mark invalid
validateFailSnaps[row] = EditableValueHolderState.create(input);
}
});
// ── 5d. UPDATE_MODEL_VALUES ────────────────────────────────────────────
// Simulates UIInput.updateModel() succeeding: checks isValid + isLocalValueSet,
// writes to EL model (skipped here), then resets the local value.
// The reset path (setValue(null) + setLocalValueSet(false)) is the hot path
// because setValue(null) internally also calls setLocalValueSet(true),
// causing an add-then-remove cycle in TransientStateHelper.
header("Full row-switch — UPDATE_MODEL_VALUES (" + ROWS + " rows)");
// Seed snapshots as if PROCESS_VALIDATIONS success ran: each row has a local value.
EditableValueHolderState[] updateSnaps = new EditableValueHolderState[ROWS];
for (int i = 0; i < ROWS; i++)
{
input.setValue(SUBMITTED_VALUES[i]); // also sets localValueSet=true
updateSnaps[i] = EditableValueHolderState.create(input);
}
resetInput(input);
run("restore → updateModel (setValue(null) + setLocalValueSet(false)) → save", () ->
{
for (int row = 0; row < ROWS; row++)
{
EditableValueHolderState evh = updateSnaps[row];
if (evh != null)
{
evh.restoreState(input);
}
else
{
EditableValueHolderState.EMPTY.restoreState(input);
}
// Simulate UIInput.updateModel() success path:
if (input.isValid() && input.isLocalValueSet())
{
// expression.setValue(context.getELContext(), getLocalValue()) skipped
input.setValue(null);
input.setLocalValueSet(false);
}
updateSnaps[row] = EditableValueHolderState.create(input);
}
});
System.out.println();
}
}