Stefan Tataru created FELIX-6863:
------------------------------------

             Summary: 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
         Attachments: default_karaf_after_jvm_restart-1.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, 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:
{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 recording to finish

What to look for in the recordings
 - *default_karaf_before_jvm_restart.jfr:* shows {{ArrayList.hashCodeRange}} as 
the top Java method:
!image-2026-10-01-20-31-40-225.png!
 - *default_karaf_after_jvm_restart.jfr:*

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, before step {{2}} from 
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, 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}}

|Kars installed|feature:uninstall before restart|after restart|after restart, 
with the fix|
|1|1.3 s|2.3 s|1.4 s|
|300|~4 s|~22 s|~3.5 s|



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

Reply via email to