Skip to content
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

No build time init of classes used in UnsafeAccessedFieldBuildItem #39831

Merged
merged 1 commit into from
Apr 4, 2024

Conversation

zakkak
Copy link
Contributor

@zakkak zakkak commented Apr 2, 2024

Use the Feature classloader instead of the default one to prevent classes being loaded to register fields as unsafe accessed to end up being build time initialized.

Resolves issues like the one described in #39819 (comment)

Note: to better understand what changed use https://github.com/quarkusio/quarkus/pull/39831/files?diff=unified&w=1

Copy link
Member

@jponge jponge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I understand correctly, I can revert to UnsafeAccessedFieldBuildItem in #39830 right?

Copy link
Member

@jponge jponge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not the expert here but it looks safe to merge

@jponge jponge requested a review from geoand April 2, 2024 13:55
@jponge
Copy link
Member

jponge commented Apr 2, 2024

I think it should eventually be backported at least down to 3.8

@jponge
Copy link
Member

jponge commented Apr 2, 2024

@zakkak I've done a check on my branch for #39830, reverting to using UnsafeAccessedFieldBuildItem and using your change.

The Mutiny native tests fail on 17 (./mvnw clean verify -f integration-tests/mutiny-native-jctools -Pnative):

Error: Classes that should be initialized at run time got initialized during image building:
 org.jctools.util.UnsafeAccess the class was requested to be initialized at run time (from feature io.quarkus.runner.Feature.beforeAnalysis with ''). org.jctools.queues.unpadded.BaseMpscLinkedUnpaddedArrayQueueProducerFields caused initialization of this class with the following trace:
	at org.jctools.util.UnsafeAccess.<clinit>(UnsafeAccess.java:44)
	at org.jctools.queues.unpadded.BaseMpscLinkedUnpaddedArrayQueueProducerFields.<clinit>(BaseMpscLinkedUnpaddedArrayQueue.java:43)


com.oracle.svm.core.util.UserError$UserException: Classes that should be initialized at run time got initialized during image building:
 org.jctools.util.UnsafeAccess the class was requested to be initialized at run time (from feature io.quarkus.runner.Feature.beforeAnalysis with ''). org.jctools.queues.unpadded.BaseMpscLinkedUnpaddedArrayQueueProducerFields caused initialization of this class with the following trace:
	at org.jctools.util.UnsafeAccess.<clinit>(UnsafeAccess.java:44)
	at org.jctools.queues.unpadded.BaseMpscLinkedUnpaddedArrayQueueProducerFields.<clinit>(BaseMpscLinkedUnpaddedArrayQueue.java:43)


	at org.graalvm.nativeimage.builder/com.oracle.svm.core.util.UserError.abort(UserError.java:73)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.classinitialization.ProvenSafeClassInitializationSupport.checkDelayedInitialization(ProvenSafeClassInitializationSupport.java:277)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.classinitialization.ClassInitializationFeature.duringAnalysis(ClassInitializationFeature.java:164)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGenerator.lambda$runPointsToAnalysis$10(NativeImageGenerator.java:770)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.FeatureHandler.forEachFeature(FeatureHandler.java:89)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGenerator.lambda$runPointsToAnalysis$11(NativeImageGenerator.java:770)
	at org.graalvm.nativeimage.pointsto/com.oracle.graal.pointsto.AbstractAnalysisEngine.runAnalysis(AbstractAnalysisEngine.java:179)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGenerator.runPointsToAnalysis(NativeImageGenerator.java:767)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGenerator.doRun(NativeImageGenerator.java:582)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGenerator.run(NativeImageGenerator.java:539)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.buildImage(NativeImageGeneratorRunner.java:408)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.build(NativeImageGeneratorRunner.java:612)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.start(NativeImageGeneratorRunner.java:134)
	at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.main(NativeImageGeneratorRunner.java:94)

Here's the diff:

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java
index 7fff7025f30..6f86d02d3dd 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java
@@ -80,25 +80,30 @@ public void write(String s, byte[] bytes) {
 
         ResultHandle beforeAnalysisParam = beforeAn.getMethodParam(0);
 
-        MethodCreator registerAsUnsafeAccessed = file
-                .getMethodCreator("registerAsUnsafeAccessed", void.class, Feature.BeforeAnalysisAccess.class)
-                .setModifiers(Modifier.PRIVATE | Modifier.STATIC);
-        for (UnsafeAccessedFieldBuildItem unsafeAccessedField : unsafeAccessedFields) {
-            TryBlock tc = registerAsUnsafeAccessed.tryBlock();
-            ResultHandle declaringClassHandle = tc.invokeStaticMethod(
-                    ofMethod(Class.class, "forName", Class.class, String.class),
-                    tc.load(unsafeAccessedField.getDeclaringClass()));
-            ResultHandle fieldHandle = tc.invokeVirtualMethod(
-                    ofMethod(Class.class, "getDeclaredField", Field.class, String.class), declaringClassHandle,
-                    tc.load(unsafeAccessedField.getFieldName()));
-            tc.invokeInterfaceMethod(
-                    ofMethod(Feature.BeforeAnalysisAccess.class, "registerAsUnsafeAccessed", void.class, Field.class),
-                    registerAsUnsafeAccessed.getMethodParam(0), fieldHandle);
-            CatchBlockCreator cc = tc.addCatch(Throwable.class);
-            cc.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cc.getCaughtException());
+        if (!unsafeAccessedFields.isEmpty()) {
+            MethodCreator registerAsUnsafeAccessed = file
+                    .getMethodCreator("registerAsUnsafeAccessed", void.class, Feature.BeforeAnalysisAccess.class)
+                    .setModifiers(Modifier.PRIVATE | Modifier.STATIC);
+            ResultHandle thisClass = registerAsUnsafeAccessed.loadClassFromTCCL(GRAAL_FEATURE);
+            ResultHandle cl = registerAsUnsafeAccessed
+                    .invokeVirtualMethod(ofMethod(Class.class, "getClassLoader", ClassLoader.class), thisClass);
+            for (UnsafeAccessedFieldBuildItem unsafeAccessedField : unsafeAccessedFields) {
+                TryBlock tc = registerAsUnsafeAccessed.tryBlock();
+                ResultHandle declaringClassHandle = tc.invokeStaticMethod(
+                        ofMethod(Class.class, "forName", Class.class, String.class, boolean.class, ClassLoader.class),
+                        tc.load(unsafeAccessedField.getDeclaringClass()), tc.load(false), cl);
+                ResultHandle fieldHandle = tc.invokeVirtualMethod(
+                        ofMethod(Class.class, "getDeclaredField", Field.class, String.class), declaringClassHandle,
+                        tc.load(unsafeAccessedField.getFieldName()));
+                tc.invokeInterfaceMethod(
+                        ofMethod(Feature.BeforeAnalysisAccess.class, "registerAsUnsafeAccessed", void.class, Field.class),
+                        registerAsUnsafeAccessed.getMethodParam(0), fieldHandle);
+                CatchBlockCreator cc = tc.addCatch(Throwable.class);
+                cc.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cc.getCaughtException());
+            }
+            registerAsUnsafeAccessed.returnVoid();
+            overallCatch.invokeStaticMethod(registerAsUnsafeAccessed.getMethodDescriptor(), beforeAnalysisParam);
         }
-        registerAsUnsafeAccessed.returnVoid();
-        overallCatch.invokeStaticMethod(registerAsUnsafeAccessed.getMethodDescriptor(), beforeAnalysisParam);
 
         overallCatch.invokeStaticMethod(BUILD_TIME_INITIALIZATION,
                 overallCatch.marshalAsArray(String.class, overallCatch.load(""))); // empty string means initialize everything
@@ -188,4 +193,4 @@ public void write(String s, byte[] bytes) {
         file.close();
     }
 
-}
+}
\ No newline at end of file
diff --git a/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java b/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java
index 0802ea01dc8..b31d1958752 100644
--- a/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java
+++ b/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java
@@ -1,5 +1,6 @@
 package io.quarkus.mutiny.deployment;
 
+import java.util.List;
 import java.util.Optional;
 
 import org.jboss.threads.ContextHandler;
@@ -10,6 +11,7 @@
 import io.quarkus.deployment.builditem.ContextHandlerBuildItem;
 import io.quarkus.deployment.builditem.ExecutorBuildItem;
 import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.UnsafeAccessedFieldBuildItem;
 import io.quarkus.mutiny.runtime.MutinyInfrastructure;
 
 public class MutinyProcessor {
@@ -31,4 +33,12 @@ public void buildTimeInit(MutinyInfrastructure recorder) {
         recorder.configureThreadBlockingChecker();
         recorder.configureOperatorLogger();
     }
+
+    @BuildStep
+    public List<UnsafeAccessedFieldBuildItem> jctoolsUnsafeAccessedFields() {
+        return List.of(
+                new UnsafeAccessedFieldBuildItem(
+                        "org.jctools.util.UnsafeRefArrayAccess",
+                        "REF_ELEMENT_SHIFT"));
+    }
 }

@zakkak
Copy link
Contributor Author

zakkak commented Apr 2, 2024

So if I understand correctly, I can revert to UnsafeAccessedFieldBuildItem in #39830 right?

Yes that was the idea.

The Mutiny native tests fail on 17 (./mvnw clean verify -f integration-tests/mutiny-native-jctools -Pnative):

Thanks for trying this. I only tested with the reproducer from #39819 which worked for me. I will have a look at the mutiny failure and get back.

This comment has been minimized.

@zakkak zakkak force-pushed the 2024-04-02-fix-registerAsUnsafeAccessed branch from a2407bc to d658f77 Compare April 2, 2024 19:16
@zakkak
Copy link
Contributor Author

zakkak commented Apr 2, 2024

That was an interesting one... The order in which you configure the unsafe accesses in regards to the order you configure class initialization is important (see https://github.com/quarkusio/quarkus/pull/39831/files?diff=unified&w=1#diff-7d1388bbdd07a9362ef261153d5cc4cbccd0f5f5f95446ad3f647f9cd38768bcR160-R162)

@jponge with this update the native image build succeeds but we still need https://github.com/quarkusio/quarkus/pull/39830/files#diff-cb8a750bd3e4b8f2bafd28b98718213cc1b2e328fbd53822ae9361f5038b5375 to avoid the segmentation faults.

At this point I really can't tell whether https://github.com/quarkusio/quarkus/pull/39830/files#diff-4fde0a5712f7c9b1f42e991f2e26671023e1a8944e68ae63f507b8a59657de0a needs to be applied or not. It's not clear to me what it does.

I am also a bit concerned that we might have other issues waiting to appear as jctools uses unsafe quite extensively.

In any case, regardless of the above, this PR appears to be in the right direction. Please have another look.

@zakkak zakkak requested review from jponge and geoand April 2, 2024 19:21
@jponge
Copy link
Member

jponge commented Apr 2, 2024

That was an interesting one... The order in which you configure the unsafe accesses in regards to the order you configure class initialization is important (see https://github.com/quarkusio/quarkus/pull/39831/files?diff=unified&w=1#diff-7d1388bbdd07a9362ef261153d5cc4cbccd0f5f5f95446ad3f647f9cd38768bcR160-R162)

That's a fun one indeed!

@jponge with this update the native image build succeeds but we still need https://github.com/quarkusio/quarkus/pull/39830/files#diff-cb8a750bd3e4b8f2bafd28b98718213cc1b2e328fbd53822ae9361f5038b5375 to avoid the segmentation faults.

👍

At this point I really can't tell whether https://github.com/quarkusio/quarkus/pull/39830/files#diff-4fde0a5712f7c9b1f42e991f2e26671023e1a8944e68ae63f507b8a59657de0a needs to be applied or not. It's not clear to me what it does.

I initially needed this when Mutiny started using JCTools to fix the native compilation on that particular class/field.

I am also a bit concerned that we might have other issues waiting to appear as jctools uses unsafe quite extensively.

  1. In the of Upgrade to Vert.x 4.4.9 with backports #39788 (3.2 branch) we are on a Mutiny version prior to the adoption of JCTools, it's a case of gRPC code doing Unsafe access that we had no problem with before / I don't know if this is related to bumping Netty/Vert.x yet but it's very likely the case.
  2. JCTools queues should only have this case to fix, the rest seems to be cases that GraalVM heuristics handle fine.
  3. @franz1981 recently merged a new variant of Unsafe-free unpadded queues to JCTools, which is what Mutiny will switch to as soon as there is a new release (but I have no ETA). If other JCTools-related issues popup due to Mutiny bringing this dependency I always have the option to use a Unsafe-free (but padded, so consuming a little more memory) variant for native compilation until we have a new JCTools release.

In any case, regardless of the above, this PR appears to be in the right direction. Please have another look.

I will do, thanks!

@franz1981
Copy link
Contributor

I would check @zakkak if native image would take care of removing the unused padding fields we have on JCTools. In case it will, using the padded variant won't be a big deal for native image (but it will, for JVM mode...)

@jponge
Copy link
Member

jponge commented Apr 2, 2024

I would check @zakkak if native image would take care of removing the unused padding fields we have on JCTools. In case it will, using the padded variant won't be a big deal for native image (but it will, for JVM mode...)

True and:

  1. Mutiny has a queues factory, so we can always have a native vs JVM flag to select the best variant, and
  2. Not many operators use queues, just to put things in perspective

@franz1981
Copy link
Contributor

FYI netty/netty#13945 freshly created; especially with the jboss executor (which can still use fast thread locals Recyclers while allocating pooled ByteBuf on Jackson extension) the new JCTools release can further reduce our memory footprint

This comment has been minimized.

@zakkak
Copy link
Contributor Author

zakkak commented Apr 3, 2024

I am looking at the breaking tests. It looks like the change moved some classes from being build-time initialized to run-time initialized and vice versa and we now need changes like https://github.com/quarkusio/quarkus/pull/39830/files#diff-cb8a750bd3e4b8f2bafd28b98718213cc1b2e328fbd53822ae9361f5038b5375 in more places.

@jponge
Copy link
Member

jponge commented Apr 3, 2024

@zakkak note that #39830 has a few extra runtime-init declarations beyond the JCTools substitution, we might go ahead and merge it

@zakkak
Copy link
Contributor Author

zakkak commented Apr 3, 2024

@zakkak note that #39830 has a few extra runtime-init declarations beyond the JCTools substitution, we might go ahead and merge it

Sure, the two patches seem orthogonal at this point.

@zakkak
Copy link
Contributor Author

zakkak commented Apr 3, 2024

I would check @zakkak if native image would take care of removing the unused padding fields we have on JCTools. In case it will, using the padded variant won't be a big deal for native image (but it will, for JVM mode...)

@franz1981 That's a good idea, but how can I test it? I did a search for unpadded in the Quarkus code base but didn't find any references. I guess that the padded/unpadded choice is made by mutiny which I can't easily alter, right?

I could try with a minimal example that directly uses JCTools but would that be enough?

@franz1981
Copy link
Contributor

These are the type of fields which are used as padding: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/BaseMpscLinkedArrayQueue.java#L34

IDK how is possible to inspect the layout (similar to what JOL) of the user defined classes which native image compile (and likely, educated guess..) strip of unreachable/unused fields, to shrink them. Maybe is an allucination of mine, but I remember that native image could perform such optimization

@jponge
Copy link
Member

jponge commented Apr 3, 2024

I would check @zakkak if native image would take care of removing the unused padding fields we have on JCTools. In case it will, using the padded variant won't be a big deal for native image (but it will, for JVM mode...)

@franz1981 That's a good idea, but how can I test it? I did a search for unpadded in the Quarkus code base but didn't find any references. I guess that the padded/unpadded choice is made by mutiny which I can't easily alter, right?

I could try with a minimal example that directly uses JCTools but would that be enough?

Mutiny currently uses the unpadded variants in https://github.com/JCTools/JCTools/tree/master/jctools-core/src/main/java/org/jctools/queues/unpadded (but they use Unsafe). It's not too hard to change in Mutiny since there is a single queues factory, so we can easily swap with the atomic variants in https://github.com/JCTools/JCTools/tree/master/jctools-core/src/main/java/org/jctools/queues/atomic (but they have padding). Now @franz1981 recently added atomic + unpadded variants in https://github.com/JCTools/JCTools/tree/master/jctools-core/src/main/java/org/jctools/queues/atomic/unpadded which will land in the next JCTools release, and that I will immediately use in Mutiny since they will be both unpadded (less GC pressure) and Unsafe-free (native friendly).

Does that make sense?

@zakkak
Copy link
Contributor Author

zakkak commented Apr 3, 2024

@franz1981 I created https://github.com/zakkak/issue-reproducers/tree/2024-04-03-jctools-test to demonstrate that the padding fields are indeed being eliminated, please have a look at the readme.

Use the Feature classloader instead of the default one to prevent
classes being loaded to register fields as unsafe accessed to end up
being build time initialized.

Similarly ensure that the registration is done after class
initialization configuration to prevent classes from being configured
for runtime initialization because of the unsafe access registration.
@zakkak zakkak force-pushed the 2024-04-02-fix-registerAsUnsafeAccessed branch from 12ccf6d to 07f7063 Compare April 3, 2024 09:29
@jponge
Copy link
Member

jponge commented Apr 3, 2024

That's super interesting @zakkak 👍

@franz1981
Copy link
Contributor

Thanks @zakkak for this so our educated guesses seems to hold, for the good and worse, because it means that padding is just some useless crap for native image, unless configured to not ignore it :/

Said that, the sole case where it matters, performance wise, is on the unbounded mpsc queues used as task mailboxes attached to the Netty event loops (1:1 per event loop); the others shouldn't be that highly contended...
My suggestion is to try having less classes to load (no multiple flavours of JCtools queues, for native image, given that the padding just evaporates)

@jponge
Copy link
Member

jponge commented Apr 3, 2024

@franz1981 For Mutiny the strategy is to switch to atomic/unpadded as soon as there is a release. I don't think it's worth rushing a switch to atomic/padded in the interim.

Copy link

quarkus-bot bot commented Apr 3, 2024

Status for workflow Quarkus CI

This is the status report for running Quarkus CI on commit 07f7063.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.


Flaky tests - Develocity

⚙️ JVM Tests - JDK 17

📦 core/deployment

io.quarkus.deployment.dev.FileSystemWatcherTestCase.testFileSystemWatcher - History

  • expected: not <null> - org.opentest4j.AssertionFailedError
org.opentest4j.AssertionFailedError: expected: not <null>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertNotNull.failNull(AssertNotNull.java:49)
	at org.junit.jupiter.api.AssertNotNull.assertNotNull(AssertNotNull.java:35)
	at org.junit.jupiter.api.AssertNotNull.assertNotNull(AssertNotNull.java:30)
	at org.junit.jupiter.api.Assertions.assertNotNull(Assertions.java:304)
	at io.quarkus.deployment.dev.FileSystemWatcherTestCase.consumeEvents(FileSystemWatcherTestCase.java:172)

@zakkak zakkak merged commit df89ad1 into main Apr 4, 2024
100 checks passed
@zakkak zakkak deleted the 2024-04-02-fix-registerAsUnsafeAccessed branch April 4, 2024 09:31
@quarkus-bot quarkus-bot bot added this to the 3.10 - main milestone Apr 4, 2024
@gsmet gsmet modified the milestones: 3.10 - main, 3.9.3 Apr 9, 2024
@gsmet gsmet modified the milestones: 3.9.3, 3.8.4 Apr 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants