[ 
https://issues.apache.org/jira/browse/FELIX-6863?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Stefan Tataru updated FELIX-6863:
---------------------------------
    Attachment: default_karaf_after_jvm_restart.jfr

> ResourceImpl.hashCode is not cached and can make Karaf feature 
> install/uninstall much slower after a JVM restart
> ----------------------------------------------------------------------------------------------------------------
>
>                 Key: FELIX-6863
>                 URL: https://issues.apache.org/jira/browse/FELIX-6863
>             Project: Felix
>          Issue Type: Improvement
>          Components: Utils
>    Affects Versions: utils-1.11.8
>         Environment: Apache Karaf 4.4.11 (binary distribution)
> OpenJDK 64-Bit Server VM, Java 21.0.7 
> OS: macOS 26.6.2, Apple Silicon, aarch64
>            Reporter: Stefan Tataru
>            Priority: Major
>              Labels: performance
>         Attachments: default_karaf_after_jvm_restart.jfr, 
> default_karaf_before_jvm_restart.jfr, 
> feature-uninstall-measurements_300_kars.md, generate_repro.py, 
> image-2026-10-01-20-31-40-225.png, image-2026-10-02-13-11-49-447.png, 
> measure_feature_uninstall.py, org.apache.karaf.features.core-4.4.11.jar
>
>
> h3. *Problem*
> [ResourceImpl.hashCode|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L131]
>  recomputes [Objects.hash(caps, 
> reqs)|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L132]
>  on every invocation: both lists are walked and {{hashCode}} is called on 
> every capability and requirement in them. In Karaf, 
> [ResourceImpl|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L34]
>  instances are used as keys in hash-based collections. For example, 
> [SubsystemResolveContext|https://github.com/apache/karaf/blob/6e6f9ae0c009eb0e0bc935acc5869d952c24b109/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java#L69]
>  maintains a [HashMap<Resource, 
> Subsystem>|https://github.com/apache/karaf/blob/dfdbc66d42b0e2ab490cce063a25494127d15b13/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java#L76]
>  into which 
> [ResourceImpl|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L34]
>  instances are inserted: [ResourceImpl wrapped = new 
> ResourceImpl()|https://github.com/apache/karaf/blob/dfdbc66d42b0e2ab490cce063a25494127d15b13/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java#L381]
>  → [resToSub.put(wrapped, 
> subsystem)|https://github.com/apache/karaf/blob/dfdbc66d42b0e2ab490cce063a25494127d15b13/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java#L392].
>  The same map is subsequently queried via 
> [resToSub.get(resource)|https://github.com/apache/karaf/blob/dfdbc66d42b0e2ab490cce063a25494127d15b13/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java#L268]
>  from: 
> [SubsystemResolveContext.getRegion|https://github.com/apache/karaf/blob/dfdbc66d42b0e2ab490cce063a25494127d15b13/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java#L271]
>  → 
> [SubsystemResolveContext.getSubsystem|https://github.com/apache/karaf/blob/dfdbc66d42b0e2ab490cce063a25494127d15b13/features/core/src/main/java/org/apache/karaf/features/internal/region/SubsystemResolveContext.java#L267].
>  For non-null keys, {{HashMap}} calls the key's {{hashCode}} on every {{put}} 
> and {{get}}, causing 
> [ResourceImpl.hashCode|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L131]
>  to be invoked on every such operation. On its own, this is just repeated 
> work, but its cost can vary a lot between JVM runs, depending on how the JIT 
> compiles the {{hashCode}} calls involved.
> h3. *Impact*
> I'm not very familiar with HotSpot's JIT compiler. I ran some experiments, 
> and an LLM helped me analyze the results, particularly the JIT behavior 
> described in the paragraph below. Please correct me if I got anything wrong.
> [Objects.hash(caps, 
> reqs)|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L132]
>  ends up in {{ArrayList.hashCode}} for each of the two lists. 
> {{ArrayList.hashCode}} calls {{ArrayList.hashCodeRange}}, which loops over 
> the elements and calls {{e.hashCode}}. 
> [CapabilityImpl|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/CapabilityImpl.java#L32]
>  and 
> [RequirementImpl|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/RequirementImpl.java#L31]
>  don’t override {{hashCode}}, so each of those calls is an 
> {{Object.hashCode}} call. The JIT decides how to compile the {{e.hashCode}} 
> call inside {{ArrayList.hashCodeRange}} from the classes it saw there while 
> the JVM was starting. If it mostly saw one unrelated class, the 
> [CapabilityImpl|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/CapabilityImpl.java#L32]
>  and 
> [RequirementImpl|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/RequirementImpl.java#L31]
>  elements end up on a slower path: a real call into the native 
> {{Object.hashCode}} for every element, instead of a few inlined instructions 
> that read the hash from the object header. In my runs it could stay like that 
> for as long as the JVM was running.
> On my machine, the steps below consistently reproduce this worst case: 
> {{feature:install}} and {{feature:uninstall}} take ~4 seconds before a JVM 
> restart and ~22 seconds after, with no change in what is installed.
> h3. *Steps to reproduce*
>  # Download and unzip 
> [apache-karaf-4.4.11|https://archive.apache.org/dist/karaf/4.4.11/apache-karaf-4.4.11.zip]
>  # Enable the {{karaf}} user for {{bin/client}}. In 
> {{apache-karaf-4.4.11/etc/users.properties}}, uncomment these two lines:
> {code:none}
> karaf = karaf,g:admingroup
> g\:admingroup = group,admin,manager,viewer,systembundles,ssh
> {code}
>  # Start Karaf and, in the Karaf console, add the Camel 4.18.2 features 
> repository:
> {code:bash}
> feature:repo-add mvn:org.apache.camel.karaf/apache-camel/4.18.2/xml/features
> {code}
>  # Install the Camel features below. This step is required to pollute the JIT 
> profile at JVM start. According to the LLM's analysis, the pollution comes 
> mainly from {{Import-Package}} entries that have a version: a fresh Karaf 
> installation has too few of them, and the generated dummy app KARs have none. 
> On my machine, the ~200 bundles these features add were enough:
> {code:bash}
> for f in camel camel-core camel-blueprint camel-spring camel-activemq 
> camel-amqp camel-bean-validator camel-bindy camel-caffeine camel-csv 
> camel-cxf camel-ftp camel-http camel-jackson camel-jaxb camel-jms 
> camel-jsonpath camel-kafka camel-mail camel-netty camel-netty-http 
> camel-quartz camel-sql camel-stream camel-xpath camel-xslt; do
>   ./apache-karaf-4.4.11/bin/client -u karaf -p karaf "feature:install -r $f" 
> || echo "FAILED: $f"
> done
> {code}
>  # Generate dummy app KAR files with the attached *generate_repro.py* script. 
> Use {{{}count 1{}}} to reproduce the problem, or {{{}count 300{}}} to amplify 
> it. The generated KARs from a single run can all be installed side by side:
> {code:bash}
> python3 generate_repro.py --count 1 --out repro-kars-1
> python3 generate_repro.py --count 300 --out repro-kars-300
> {code}
>  # Install all the generated KARs, for example by copying them to 
> {{apache-karaf-4.4.11/deploy/}}:
> {code:bash}
> cp repro-kars-300/* ./apache-karaf-4.4.11/deploy/
> {code}
>  # Wait until all the generated KARs are fully installed. In the Karaf 
> console, {{kar:list | wc}} should return the number of generated KARs + 2, 
> and all installed bundles should be in the state {{Active}}:
> {code:bash}
> kar:list | wc
> {code}
>  # (Optional) Check the {{feature:install}} and {{feature:uninstall}} 
> performance manually, for example by installing another KAR or uninstalling 
> an existing one. A quick way to time it without the script in step *9*:
> {code:bash}
> time ./apache-karaf-4.4.11/bin/client -u karaf -p karaf "feature:uninstall -t 
> -r repro-app-001"
> {code}
> *(-t only simulates the uninstall, so the command can be repeated)*
>  # Start a JFR recording, then measure {{feature:uninstall}} with the 
> attached *measure_feature_uninstall.py* script while the recording is 
> running. Run both commands together, so the measurement starts right after 
> the recording does:
> {code:bash}
> jcmd $(cat ./apache-karaf-4.4.11/karaf.pid) JFR.start duration=120s 
> settings=profile jdk.ExecutionSample#period=10ms 
> jdk.NativeMethodSample#period=10ms 
> filename=default_karaf_before_jvm_restart.jfr && \
> python3 measure_feature_uninstall.py --client 
> ./apache-karaf-4.4.11/bin/client --command "feature:uninstall -t -r 
> repro-app-001" --title "before restart" --runs 10
> {code}
>  # Wait for the JFR recording to finish
>  # Restart Karaf without cleaning the {{data}} folder, and wait until all 
> bundles are in the state {{Active}}
>  # (Optional) Check the {{feature:install}} and {{feature:uninstall}} 
> performance manually as in step *8*
>  # Repeat step *9* with:
> {code:bash}
> jcmd $(cat ./apache-karaf-4.4.11/karaf.pid) JFR.start duration=120s 
> settings=profile jdk.ExecutionSample#period=10ms 
> jdk.NativeMethodSample#period=10ms 
> filename=default_karaf_after_jvm_restart.jfr && \
> python3 measure_feature_uninstall.py --client 
> ./apache-karaf-4.4.11/bin/client --command "feature:uninstall -t -r 
> repro-app-001" --title "after restart" --runs 10
> {code}
>  # Wait for the JFR recording to finish
> What to look for in the recordings
>  - {*}default_karaf_before_jvm_restart.jfr:{*} shows 
> {{ArrayList.hashCodeRange}} as the top Java method on the *Method Profiling* 
> page in JDK Mission Control (JMC):
>  !image-2026-10-01-20-31-40-225.png!
>  - {*}default_karaf_after_jvm_restart.jfr:{*} {{ArrayList.hashCodeRange}} is 
> gone from JMC's *Method Profiling* page, but the hot path is the same. The 
> *Method Profiling* page only shows {{jdk.ExecutionSample}} events. After the 
> restart, the hot method is native, so JFR captures it through 
> {{jdk.NativeMethodSample}} events instead. To see these samples in JMC: 
> *Event Browser* → *Java Virtual Machine* → *Profiling* → *Method Profiling 
> Sample Native*. Sort by *Thread* and select all {{features-3-thread-1}} rows:
> !image-2026-10-02-13-11-49-447.png!
> h3. *Proposal*
> One way to fix this is to cache the result of 
> [ResourceImpl.hashCode|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L131]
>  and only recompute it when the [List<Capability> 
> caps|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L36]
>  or [List<Requirement> 
> reqs|https://github.com/apache/felix-dev/blob/b47fa6abb20ff10dbd86574e4df68cd959da5c26/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java#L37]
>  change.
> The fix can be tested in either of the following ways, after step *1* in the 
> "{*}Steps to reproduce{*}":
>  * Replace 
> {{/apache-karaf-4.4.11/system/org/apache/karaf/features/org.apache.karaf.features.core/4.4.11/org.apache.karaf.features.core-4.4.11.jar}}
>  with the attached jar
>  * Build {{org.apache.felix:org.apache.felix.utils}} locally from the 
> [proposed PR|https://github.com/apache/felix-dev/pull/564], then rebuild 
> [org.apache.karaf.features:org.apache.karaf.features.core|https://github.com/apache/karaf/blob/karaf-4.4.11/features/core/pom.xml]
>  against it, and replace 
> {{/apache-karaf-4.4.11/system/org/apache/karaf/features/org.apache.karaf.features.core/4.4.11/org.apache.karaf.features.core-4.4.11.jar}}
>  with the built jar
> |Kars installed|feature:uninstall before restart|after restart|after restart, 
> with the fix|
> |1|~1.3s|~2.3s|~1.4s|
> |300|~4s|~22s|~3.5s|



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to