-
Notifications
You must be signed in to change notification settings - Fork 415
Let an application component register models beside its default model #1605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
reiern70
wants to merge
4
commits into
master
Choose a base branch
from
reiern70/support-multiple-models
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,263
−15
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1073442
Let an application component register models beside its default model
reiern70 66b9bae
List the modules that were missing from the README
reiern70 f9496e9
Measure what registering additional models costs
reiern70 8af129a
Record the measured cost of registering additional models
reiern70 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| # What registering additional models costs | ||
|
|
||
| Measured comparison of the two ways a component can hold models beside its default model: | ||
|
|
||
| | Variant | Extra models held in | Detached by | | ||
| | --- | --- | --- | | ||
| | `FIELDS` | one field each | a hand-written `onDetach()` | | ||
| | `REGISTERED` | one field each | `addAdditionalModel` | | ||
| | `REGISTERED_NO_FIELDS` | nothing | `addAdditionalModel` | | ||
|
|
||
| All three carry the same default model and the same extra models, so every difference below | ||
| is bookkeeping, never the models themselves. | ||
|
|
||
| **Environment:** OpenJDK 24.0.1, `-Xmx1g`, 1,000 children per tree, JOL 0.17, JMH 1.37. | ||
| JMH run with `-f 1 -wi 3 -i 3 -r 1 -w 1` — quick mode, so treat `ns/op` as indicative and | ||
| `B/op` as exact (allocation is counted, not sampled). | ||
|
|
||
| Reproduce with `wicket-benchmarks/run-model-benchmarks.sh`. | ||
|
|
||
| --- | ||
|
|
||
| ## Memory — retained heap, bytes per component | ||
|
|
||
| Deltas against the same component carrying only a default model. | ||
|
|
||
| | Extra models | `FIELDS` | `REGISTERED` | `REGISTERED_NO_FIELDS` | registering costs | | ||
| | ---: | ---: | ---: | ---: | ---: | | ||
| | 1 | 64.0 | 136.0 | 120.0 | **+72.0** | | ||
| | 2 | 128.0 | 200.0 | 184.0 | **+72.0** | | ||
| | 3 | 192.0 | 272.0 | 256.0 | **+80.0** | | ||
|
|
||
| With `-XX:+UseCompactObjectHeaders` (every object 4 bytes smaller): | ||
|
|
||
| | Extra models | `FIELDS` | `REGISTERED` | `REGISTERED_NO_FIELDS` | registering costs | | ||
| | ---: | ---: | ---: | ---: | ---: | | ||
| | 1 | 63.9 | 119.9 | 111.9 | **+56.0** | | ||
| | 2 | 127.8 | 191.8 | 183.8 | **+64.0** | | ||
| | 3 | 191.8 | 255.8 | 247.8 | **+64.0** | | ||
|
|
||
| ## Memory — serialized bytes per component | ||
|
|
||
| What the page store pays. | ||
|
|
||
| | Extra models | `FIELDS` | `REGISTERED` | `REGISTERED_NO_FIELDS` | registering costs | | ||
| | ---: | ---: | ---: | ---: | ---: | | ||
| | 1 | 13.9 | 47.2 | 40.2 | **+33.3** | | ||
| | 2 | 27.8 | 66.1 | 55.1 | **+38.3** | | ||
| | 3 | 41.7 | 85.0 | 70.0 | **+43.3** | | ||
|
|
||
| Unaffected by compact headers, as expected — the wire format does not carry object headers. | ||
|
|
||
| ## Where the bytes go | ||
|
|
||
| JOL breakdown for `REGISTERED` with 3 extra models, 1,000 components: | ||
|
|
||
| | Object | Count | Avg | Sum | | ||
| | --- | ---: | ---: | ---: | | ||
| | `AdditionalModelsShapes$RegisteredModels` | 1000 | 72 | 72,000 | | ||
| | `[Lorg.apache.wicket.model.IModel;` | 1000 | 32 | 32,000 | | ||
| | `org.apache.wicket.ComponentState` | 1000 | 24 | 24,000 | | ||
| | `org.apache.wicket.MetaDataEntry` | 1000 | 24 | 24,000 | | ||
| | `org.apache.wicket.model.Model` | 4000 | 16 | 64,000 | | ||
|
|
||
| The three objects a registered component pays for are exactly `ComponentState` (24) + | ||
| `MetaDataEntry` (24) + the `IModel[]` (32) = **80 bytes** — the measured `+80.0` at three | ||
| models. The array is the only part that grows: 24 bytes at one model, 32 at three. | ||
|
|
||
| ## Time and allocation | ||
|
|
||
| `build` — constructing the component, where registering does its extra work: | ||
|
|
||
| | Extra models | Variant | ns/op | B/op | | ||
| | ---: | --- | ---: | ---: | | ||
| | 1 | `FIELDS` | 26.2 ± 2.2 | 152 | | ||
| | 1 | `REGISTERED` | 33.9 ± 12.6 | 224 | | ||
| | 1 | `REGISTERED_NO_FIELDS` | 32.2 ± 2.0 | 208 | | ||
| | 2 | `FIELDS` | 35.6 ± 2.0 | 216 | | ||
| | 2 | `REGISTERED` | 54.0 ± 4.7 | 312 | | ||
| | 2 | `REGISTERED_NO_FIELDS` | 54.4 ± 3.9 | 296 | | ||
| | 3 | `FIELDS` | 41.9 ± 1.9 | 280 | | ||
| | 3 | `REGISTERED` | 75.6 ± 10.7 | 408 | | ||
| | 3 | `REGISTERED_NO_FIELDS` | 76.5 ± 15.8 | 392 | | ||
|
|
||
| `buildAndDetach` — the whole per-request cycle: | ||
|
|
||
| | Extra models | Variant | ns/op | B/op | | ||
| | ---: | --- | ---: | ---: | | ||
| | 1 | `FIELDS` | 31.8 ± 2.8 | 152 | | ||
| | 1 | `REGISTERED` | 39.8 ± 11.0 | 224 | | ||
| | 1 | `REGISTERED_NO_FIELDS` | 38.8 ± 1.2 | 208 | | ||
| | 2 | `FIELDS` | 39.1 ± 2.0 | 216 | | ||
| | 2 | `REGISTERED` | 60.4 ± 1.7 | 312 | | ||
| | 2 | `REGISTERED_NO_FIELDS` | 61.2 ± 2.1 | 296 | | ||
| | 3 | `FIELDS` | 47.0 ± 5.6 | 280 | | ||
| | 3 | `REGISTERED` | 82.5 ± 5.4 | 408 | | ||
| | 3 | `REGISTERED_NO_FIELDS` | 88.4 ± 3.6 | 392 | | ||
|
|
||
| --- | ||
|
|
||
| ## Conclusions | ||
|
|
||
| **1. The documented "50 to 80 bytes" is right, and slightly conservative at the top end.** | ||
| Retained heap costs **72 to 80 bytes per component** with default headers, **56 to 64** with | ||
| compact ones. The guide and the `addAdditionalModel` javadoc can stand as they are. | ||
|
|
||
| **2. The cost is near-flat in the number of models, as claimed — but not exactly flat.** | ||
| 72, 72, 80 bytes for one, two and three models. The fixed part is the `MetaDataEntry` plus | ||
| the `ComponentState` a component needs once it has more than one kind of state; only the | ||
| `IModel[]` grows, and it grows by 8 bytes per few models, not per model. Wording like | ||
| "whatever their number" is fair for heap. It is weaker for the wire: **+33, +38, +43 | ||
| bytes**, about 5 bytes per model, because serialization writes the array and its class | ||
| descriptor. | ||
|
|
||
| **3. Serialized cost is the one that bites.** A registered component is **3.4× to 2.0×** | ||
| the serialized size of the hand-detached one (47.2 vs 13.9 bytes at one model). Small in | ||
| absolute terms, but the page store pays it on every page write, for every instance. | ||
|
|
||
| **4. Construction is measurably slower, and that is the real per-instance price.** | ||
| `build` goes from 41.9 to 75.6 ns/op at three models — roughly **+80%**, well outside the | ||
| error bars. Allocation confirms it exactly: **+128 B/op** at three models. This is the | ||
| number to weigh for a component rendered in the thousands, and it is a stronger argument | ||
| than the heap figure that the components shipped with Wicket should keep detaching their | ||
| own models. | ||
|
|
||
| **5. Detaching itself is cheaper when registered, which partly offsets construction.** | ||
| `buildAndDetach` minus `build` is ~5 ns for `FIELDS` against ~4–7 ns for `REGISTERED`: the | ||
| framework's loop over the model array costs about what three hand-written null-checked | ||
| `detach()` calls cost. Registering buys its convenience at construction time, not at detach | ||
| time. | ||
|
|
||
| **6. Dropping the fields is worth 16 bytes of heap and 11–15 bytes on the wire.** | ||
| `REGISTERED_NO_FIELDS` beats `REGISTERED` at every model count, and the time difference is | ||
| within the noise. A component that only passes models to its children should use the | ||
| `(String, IModel, IModel...)` constructor rather than keeping fields it never reads — an | ||
| option the hand-detached approach cannot offer at all, since it needs the fields to reach | ||
| the models from `onDetach()`. | ||
|
|
||
| **Where the trade stops paying.** At ~80 bytes of heap and ~43 of wire per component, a | ||
| panel used 50 times on a page costs 4KB of heap — irrelevant. The same feature on a cell | ||
| rendered 10,000 times in a `DataTable` costs 800KB of heap, 430KB per serialized page, and | ||
| roughly 0.34ms of extra construction per render. The existing guidance — register in | ||
| application components, detach by hand in components rendered in the thousands — is | ||
| supported by these numbers. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # 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. | ||
| # | ||
| # Runs the additional-models footprint and JMH benchmarks and prints the raw output. | ||
| # bash run-model-benchmarks.sh quick (~4 min, 1 fork) | ||
| # bash run-model-benchmarks.sh full full rigor (~15 min, 3 forks) | ||
|
|
||
| # re-exec under bash when started with sh: dash has no pipefail | ||
| if [ -z "${BASH_VERSION:-}" ]; then exec bash "$0" "$@"; fi | ||
|
|
||
| set -euo pipefail | ||
| cd "$(dirname "$(readlink -f "$0")")/.." | ||
|
|
||
| MODE="${1:-quick}" | ||
| OUT=target/model-benchmarks.txt | ||
| mkdir -p target | ||
|
|
||
| echo "### building (quiet) ..." >&2 | ||
| mvn -o -q clean -pl wicket-core | ||
| mvn -o -q -pl wicket-benchmarks -am compile | ||
| # Copy the dependency jars into the module, rather than referencing ~/.m2: a confined | ||
| # process (Claude Code's snap) can read the repo but not hidden directories under $HOME. | ||
| if [ ! -d wicket-benchmarks/target/deps ]; then | ||
| mvn -o -q -pl wicket-benchmarks dependency:copy-dependencies \ | ||
| -DoutputDirectory=wicket-benchmarks/target/deps \ | ||
| || mvn -q -pl wicket-benchmarks dependency:copy-dependencies \ | ||
| -DoutputDirectory=wicket-benchmarks/target/deps | ||
| fi | ||
|
|
||
| CP="wicket-benchmarks/target/classes:wicket-core/target/classes:wicket-util/target/classes:\ | ||
| wicket-request/target/classes:wicket-tester/target/classes:wicket-benchmarks/target/deps/*" | ||
|
|
||
| { | ||
| echo "===== JOL FOOTPRINT, default headers =====" | ||
| java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$CP" \ | ||
| org.apache.wicket.benchmarks.AdditionalModelsFootprint | ||
|
|
||
| echo | ||
| echo "===== JOL FOOTPRINT, -XX:+UseCompactObjectHeaders =====" | ||
| java -XX:+UseCompactObjectHeaders --add-opens java.base/java.lang=ALL-UNNAMED -cp "$CP" \ | ||
| org.apache.wicket.benchmarks.AdditionalModelsFootprint 2>&1 || \ | ||
| echo "(compact object headers not supported by this JDK)" | ||
|
|
||
| echo | ||
| echo "===== JMH, mode=$MODE =====" | ||
| if [ "$MODE" = full ]; then | ||
| java -cp "$CP" org.openjdk.jmh.Main AdditionalModelsBenchmark -prof gc -jvmArgs "-Xmx1g" | ||
| else | ||
| java -cp "$CP" org.openjdk.jmh.Main AdditionalModelsBenchmark \ | ||
| -prof gc -jvmArgs "-Xmx1g" -f 1 -wi 3 -i 3 -r 1 -w 1 | ||
| fi | ||
| } 2>&1 | tee "$OUT" | ||
|
|
||
| echo >&2 | ||
| echo "### raw output also saved to $OUT" >&2 |
95 changes: 95 additions & 0 deletions
95
wicket-benchmarks/src/main/java/org/apache/wicket/benchmarks/AdditionalModelsBenchmark.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /* | ||
| * 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 org.apache.wicket.benchmarks; | ||
|
|
||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import org.apache.wicket.Component; | ||
| import org.apache.wicket.benchmarks.AdditionalModelsShapes.Variant; | ||
| import org.openjdk.jmh.annotations.Benchmark; | ||
| import org.openjdk.jmh.annotations.BenchmarkMode; | ||
| import org.openjdk.jmh.annotations.Fork; | ||
| import org.openjdk.jmh.annotations.Measurement; | ||
| import org.openjdk.jmh.annotations.Mode; | ||
| import org.openjdk.jmh.annotations.OutputTimeUnit; | ||
| import org.openjdk.jmh.annotations.Param; | ||
| import org.openjdk.jmh.annotations.Scope; | ||
| import org.openjdk.jmh.annotations.Setup; | ||
| import org.openjdk.jmh.annotations.State; | ||
| import org.openjdk.jmh.annotations.TearDown; | ||
| import org.openjdk.jmh.annotations.Threads; | ||
| import org.openjdk.jmh.annotations.Warmup; | ||
| import org.openjdk.jmh.infra.Blackhole; | ||
|
|
||
| /** | ||
| * What it costs in time and allocation to let {@code Component} detach a component's extra models | ||
| * instead of detaching them by hand, for the shapes of {@link AdditionalModelsShapes}. | ||
| * <p> | ||
| * Two operations, because they answer different questions: | ||
| * <ul> | ||
| * <li>{@code build} - constructing the component. This is where registering does its extra work: | ||
| * a meta data write and a copy of the model array per model registered. | ||
| * <li>{@code buildAndDetach} - construct and detach, the whole per request cycle. Detaching | ||
| * mutates state, so it cannot be measured repeatedly against the same instance; folding | ||
| * construction into the operation keeps every invocation doing real work. | ||
| * </ul> | ||
| * Always run with {@code -prof gc}: {@code gc.alloc.rate.norm} is the number that decides whether | ||
| * the convenience is affordable for a component rendered in large numbers, and it is far steadier | ||
| * than throughput. For the memory a component keeps, rather than the garbage it makes, see | ||
| * {@link AdditionalModelsFootprint}. | ||
| */ | ||
| @BenchmarkMode(Mode.AverageTime) | ||
| @OutputTimeUnit(TimeUnit.NANOSECONDS) | ||
| @Fork(3) | ||
| @Threads(1) | ||
| @Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS) | ||
| @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) | ||
| @State(Scope.Thread) | ||
| public class AdditionalModelsBenchmark | ||
| { | ||
| @Param | ||
| public Variant variant; | ||
|
|
||
| @Param({ "1", "2", "3" }) | ||
| public int extraModels; | ||
|
|
||
| @Setup | ||
| public void setUp() | ||
| { | ||
| WicketContext.attach(); | ||
| } | ||
|
|
||
| @TearDown | ||
| public void tearDown() | ||
| { | ||
| WicketContext.detach(); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public Component build() | ||
| { | ||
| return variant.newComponent("c", extraModels); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void buildAndDetach(Blackhole blackhole) | ||
| { | ||
| Component component = variant.newComponent("c", extraModels); | ||
| component.detach(); | ||
| blackhole.consume(component); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@papegaaij This document contains the numbers of the new benchmarks